Commit 76b07aa

Browse files
authored
feat: add CloudWatch traces API for web UI (#997)
1 parent 29b6522 commit 76b07aa

14 files changed

Lines changed: 1244 additions & 197 deletions

File tree

‎src/cli/commands/dev/browser-mode.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
runWebUI,
1010
}from'../../operations/dev/web-ui';
1111
import{listMemoryRecords,retrieveMemoryRecords}from'../../operations/memory';
12+
import{loadDeployedProjectConfig,resolveAgent}from'../../operations/resolve-agent';
13+
import{fetchTraceRecords,listTraces}from'../../operations/traces';
1214
importpathfrom'node:path';
1315

1416
interfaceDeployedHandlers{
@@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
192194
? (agentNameParam,startTime,endTime)=>collector.listTraces(agentNameParam,startTime,endTime)
193195
: undefined,
194196
onGetTrace: collector ? (agentNameParam,traceId)=>collector.getTraceSpans(agentNameParam,traceId) : undefined,
197+
onListCloudWatchTraces: async(agentName,_harnessName,startTime,endTime)=>{
198+
try{
199+
constconfigIO=newConfigIO({ baseDir });
200+
constcontext=awaitloadDeployedProjectConfig(configIO);
201+
constresolved=resolveAgent(context,{runtime: agentName});
202+
if(!resolved.success)return{success: false,error: resolved.error};
203+
returnlistTraces({
204+
region: resolved.agent.region,
205+
runtimeId: resolved.agent.runtimeId,
206+
agentName: resolved.agent.agentName,
207+
startTime,
208+
endTime,
209+
});
210+
}catch(err){
211+
return{
212+
success: false,
213+
error: `Failed to list CloudWatch traces: ${errinstanceofError ? err.message : String(err)}`,
214+
};
215+
}
216+
},
217+
onGetCloudWatchTrace: async(agentName,_harnessName,traceId,startTime,endTime)=>{
218+
try{
219+
constconfigIO=newConfigIO({ baseDir });
220+
constcontext=awaitloadDeployedProjectConfig(configIO);
221+
constresolved=resolveAgent(context,{runtime: agentName});
222+
if(!resolved.success)return{success: false,error: resolved.error};
223+
returnfetchTraceRecords({
224+
region: resolved.agent.region,
225+
runtimeId: resolved.agent.runtimeId,
226+
traceId,
227+
startTime,
228+
endTime,
229+
includeSpans: true,
230+
});
231+
}catch(err){
232+
return{
233+
success: false,
234+
error: `Failed to get CloudWatch trace: ${errinstanceofError ? err.message : String(err)}`,
235+
};
236+
}
237+
},
195238
onListMemoryRecords: async(memoryName,namespace,strategyId)=>{
196239
constdeployed=awaitresolveDeployedHandlers(baseDir,onLog);
197240
if(!deployed.onListMemoryRecords)return{success: false,error: 'No deployed AgentCore Memory found'};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import{handleGetCloudWatchTrace,handleListCloudWatchTraces}from'../handlers/cloudwatch-traces.js';
2+
importtype{RouteContext}from'../handlers/route-context.js';
3+
importtype{IncomingMessage,ServerResponse}from'http';
4+
import{beforeEach,describe,expect,it,vi}from'vitest';
5+
6+
functionmockRes(): ServerResponse&{_status: number;_headers: Record<string,string>;_body: string}{
7+
constres={
8+
_status: 0,
9+
_headers: {}asRecord<string,string>,
10+
_body: '',
11+
writeHead(status: number,headers?: Record<string,string>){
12+
res._status=status;
13+
if(headers)Object.assign(res._headers,headers);
14+
returnres;
15+
},
16+
setHeader(name: string,value: string){
17+
res._headers[name]=value;
18+
},
19+
end(body?: string){
20+
if(body)res._body=body;
21+
},
22+
};
23+
returnresasunknownasServerResponse&{_status: number;_headers: Record<string,string>;_body: string};
24+
}
25+
26+
functionmockReq(url: string): IncomingMessage{
27+
return{ url,headers: {host: 'localhost:8081'}}asunknownasIncomingMessage;
28+
}
29+
30+
functionmockCtx(overrides: Partial<RouteContext['options']>={}): RouteContext{
31+
return{
32+
options: {
33+
mode: 'dev',
34+
agents: [],
35+
harnesses: [],
36+
uiPort: 8081,
37+
...overrides,
38+
},
39+
runningAgents: newMap(),
40+
startingAgents: newMap(),
41+
agentErrors: newMap(),
42+
setCorsHeaders: vi.fn(),
43+
readBody: vi.fn(),
44+
}asRouteContext;
45+
}
46+
47+
describe('handleListCloudWatchTraces',()=>{
48+
beforeEach(()=>{
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns 404 when no handler configured',async()=>{
53+
constctx=mockCtx();
54+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
55+
constres=mockRes();
56+
57+
awaithandleListCloudWatchTraces(ctx,req,res);
58+
59+
expect(res._status).toBe(404);
60+
constbody=JSON.parse(res._body);
61+
expect(body.success).toBe(false);
62+
expect(body.error).toContain('not available');
63+
});
64+
65+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
66+
consthandler=vi.fn();
67+
constctx=mockCtx({onListCloudWatchTraces: handler});
68+
constreq=mockReq('/api/cloudwatch-traces');
69+
constres=mockRes();
70+
71+
awaithandleListCloudWatchTraces(ctx,req,res);
72+
73+
expect(res._status).toBe(400);
74+
constbody=JSON.parse(res._body);
75+
expect(body.success).toBe(false);
76+
expect(body.error).toContain('agentName');
77+
expect(body.error).toContain('harnessName');
78+
expect(handler).not.toHaveBeenCalled();
79+
});
80+
81+
it('returns 400 when both agentName and harnessName provided',async()=>{
82+
consthandler=vi.fn();
83+
constctx=mockCtx({onListCloudWatchTraces: handler});
84+
constreq=mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h');
85+
constres=mockRes();
86+
87+
awaithandleListCloudWatchTraces(ctx,req,res);
88+
89+
expect(res._status).toBe(400);
90+
constbody=JSON.parse(res._body);
91+
expect(body.success).toBe(false);
92+
expect(body.error).toContain('agentName');
93+
expect(body.error).toContain('harnessName');
94+
expect(handler).not.toHaveBeenCalled();
95+
});
96+
97+
it('calls handler with agentName and returns traces',async()=>{
98+
consttraces=[{traceId: 't1'},{traceId: 't2'}];
99+
consthandler=vi.fn().mockResolvedValue({success: true, traces });
100+
constctx=mockCtx({onListCloudWatchTraces: handler});
101+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
102+
constres=mockRes();
103+
104+
awaithandleListCloudWatchTraces(ctx,req,res);
105+
106+
expect(res._status).toBe(200);
107+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,undefined,undefined);
108+
constbody=JSON.parse(res._body);
109+
expect(body.success).toBe(true);
110+
expect(body.traces).toEqual(traces);
111+
});
112+
113+
it('calls handler with harnessName',async()=>{
114+
consthandler=vi.fn().mockResolvedValue({success: true,traces: []});
115+
constctx=mockCtx({onListCloudWatchTraces: handler});
116+
constreq=mockReq('/api/cloudwatch-traces?harnessName=my-harness');
117+
constres=mockRes();
118+
119+
awaithandleListCloudWatchTraces(ctx,req,res);
120+
121+
expect(res._status).toBe(200);
122+
expect(handler).toHaveBeenCalledWith(undefined,'my-harness',undefined,undefined);
123+
});
124+
125+
it('returns 500 when handler throws',async()=>{
126+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
127+
constctx=mockCtx({onListCloudWatchTraces: handler});
128+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
129+
constres=mockRes();
130+
131+
awaithandleListCloudWatchTraces(ctx,req,res);
132+
133+
expect(res._status).toBe(500);
134+
constbody=JSON.parse(res._body);
135+
expect(body.success).toBe(false);
136+
expect(body.error).toContain('Failed to list CloudWatch traces');
137+
});
138+
139+
it('returns 400 for invalid startTime',async()=>{
140+
consthandler=vi.fn();
141+
constctx=mockCtx({onListCloudWatchTraces: handler});
142+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber');
143+
constres=mockRes();
144+
145+
awaithandleListCloudWatchTraces(ctx,req,res);
146+
147+
expect(res._status).toBe(400);
148+
constbody=JSON.parse(res._body);
149+
expect(body.success).toBe(false);
150+
expect(body.error).toContain('startTime');
151+
expect(handler).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('handleGetCloudWatchTrace',()=>{
156+
beforeEach(()=>{
157+
vi.restoreAllMocks();
158+
});
159+
160+
it('returns 404 when no handler configured',async()=>{
161+
constctx=mockCtx();
162+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
163+
constres=mockRes();
164+
165+
awaithandleGetCloudWatchTrace(ctx,req,res);
166+
167+
expect(res._status).toBe(404);
168+
constbody=JSON.parse(res._body);
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain('not available');
171+
});
172+
173+
it('returns 400 when traceId is missing',async()=>{
174+
consthandler=vi.fn();
175+
constctx=mockCtx({onGetCloudWatchTrace: handler});
176+
constreq=mockReq('/api/cloudwatch-traces/?agentName=my-agent');
177+
constres=mockRes();
178+
179+
awaithandleGetCloudWatchTrace(ctx,req,res);
180+
181+
expect(res._status).toBe(400);
182+
constbody=JSON.parse(res._body);
183+
expect(body.success).toBe(false);
184+
expect(body.error).toContain('traceId');
185+
expect(handler).not.toHaveBeenCalled();
186+
});
187+
188+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
189+
consthandler=vi.fn();
190+
constctx=mockCtx({onGetCloudWatchTrace: handler});
191+
constreq=mockReq('/api/cloudwatch-traces/abc123');
192+
constres=mockRes();
193+
194+
awaithandleGetCloudWatchTrace(ctx,req,res);
195+
196+
expect(res._status).toBe(400);
197+
constbody=JSON.parse(res._body);
198+
expect(body.success).toBe(false);
199+
expect(body.error).toContain('agentName');
200+
expect(body.error).toContain('harnessName');
201+
expect(handler).not.toHaveBeenCalled();
202+
});
203+
204+
it('returns 500 when handler throws',async()=>{
205+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
206+
constctx=mockCtx({onGetCloudWatchTrace: handler});
207+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
208+
constres=mockRes();
209+
210+
awaithandleGetCloudWatchTrace(ctx,req,res);
211+
212+
expect(res._status).toBe(500);
213+
constbody=JSON.parse(res._body);
214+
expect(body.success).toBe(false);
215+
expect(body.error).toContain('Failed to get CloudWatch trace');
216+
});
217+
218+
it('calls handler and returns records',async()=>{
219+
constrecords=[{record: 'data1'}];
220+
consthandler=vi.fn().mockResolvedValue({success: true, records });
221+
constctx=mockCtx({onGetCloudWatchTrace: handler});
222+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
223+
constres=mockRes();
224+
225+
awaithandleGetCloudWatchTrace(ctx,req,res);
226+
227+
expect(res._status).toBe(200);
228+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,'abc123',undefined,undefined);
229+
constbody=JSON.parse(res._body);
230+
expect(body.success).toBe(true);
231+
expect(body.records).toEqual(records);
232+
});
233+
});

‎src/cli/operations/dev/web-ui/api-types.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* TODO: Extract these types into a shared package so both repos import
99
* from a single source of truth instead of manually duplicating.
1010
*/
11+
importtype{CloudWatchSpanRecord,CloudWatchTraceRecord}from'../../traces/types';
1112

1213
// ---------------------------------------------------------------------------
1314
// GET /api/status
@@ -279,6 +280,39 @@ export interface GetTraceResponse {
279280
error?: string;
280281
}
281282

283+
// ---------------------------------------------------------------------------
284+
// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx
285+
// ---------------------------------------------------------------------------
286+
287+
/** A single trace entry returned by the CloudWatch traces list endpoint */
288+
exportinterfaceCloudWatchTraceEntry{
289+
traceId: string;
290+
timestamp: string;
291+
sessionId?: string;
292+
spanCount?: string;
293+
}
294+
295+
/** Response shape for GET /api/cloudwatch-traces */
296+
exportinterfaceListCloudWatchTracesResponse{
297+
success: boolean;
298+
traces?: CloudWatchTraceEntry[];
299+
error?: string;
300+
}
301+
302+
// ---------------------------------------------------------------------------
303+
// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx
304+
// ---------------------------------------------------------------------------
305+
306+
/** Response shape for GET /api/cloudwatch-traces/:traceId */
307+
exportinterfaceGetCloudWatchTraceResponse{
308+
success: boolean;
309+
records?: CloudWatchTraceRecord[];
310+
spans?: CloudWatchSpanRecord[];
311+
error?: string;
312+
}
313+
314+
exporttype{CloudWatchTraceRecord,CloudWatchSpanRecord}from'../../traces/types';
315+
282316
// ---------------------------------------------------------------------------
283317
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
284318
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 76b07aa

Browse files
authored
feat: add CloudWatch traces API for web UI (#997)
1 parent 29b6522 commit 76b07aa

14 files changed

Lines changed: 1244 additions & 197 deletions

File tree

‎src/cli/commands/dev/browser-mode.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
runWebUI,
1010
}from'../../operations/dev/web-ui';
1111
import{listMemoryRecords,retrieveMemoryRecords}from'../../operations/memory';
12+
import{loadDeployedProjectConfig,resolveAgent}from'../../operations/resolve-agent';
13+
import{fetchTraceRecords,listTraces}from'../../operations/traces';
1214
importpathfrom'node:path';
1315

1416
interfaceDeployedHandlers{
@@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
192194
? (agentNameParam,startTime,endTime)=>collector.listTraces(agentNameParam,startTime,endTime)
193195
: undefined,
194196
onGetTrace: collector ? (agentNameParam,traceId)=>collector.getTraceSpans(agentNameParam,traceId) : undefined,
197+
onListCloudWatchTraces: async(agentName,_harnessName,startTime,endTime)=>{
198+
try{
199+
constconfigIO=newConfigIO({ baseDir });
200+
constcontext=awaitloadDeployedProjectConfig(configIO);
201+
constresolved=resolveAgent(context,{runtime: agentName});
202+
if(!resolved.success)return{success: false,error: resolved.error};
203+
returnlistTraces({
204+
region: resolved.agent.region,
205+
runtimeId: resolved.agent.runtimeId,
206+
agentName: resolved.agent.agentName,
207+
startTime,
208+
endTime,
209+
});
210+
}catch(err){
211+
return{
212+
success: false,
213+
error: `Failed to list CloudWatch traces: ${errinstanceofError ? err.message : String(err)}`,
214+
};
215+
}
216+
},
217+
onGetCloudWatchTrace: async(agentName,_harnessName,traceId,startTime,endTime)=>{
218+
try{
219+
constconfigIO=newConfigIO({ baseDir });
220+
constcontext=awaitloadDeployedProjectConfig(configIO);
221+
constresolved=resolveAgent(context,{runtime: agentName});
222+
if(!resolved.success)return{success: false,error: resolved.error};
223+
returnfetchTraceRecords({
224+
region: resolved.agent.region,
225+
runtimeId: resolved.agent.runtimeId,
226+
traceId,
227+
startTime,
228+
endTime,
229+
includeSpans: true,
230+
});
231+
}catch(err){
232+
return{
233+
success: false,
234+
error: `Failed to get CloudWatch trace: ${errinstanceofError ? err.message : String(err)}`,
235+
};
236+
}
237+
},
195238
onListMemoryRecords: async(memoryName,namespace,strategyId)=>{
196239
constdeployed=awaitresolveDeployedHandlers(baseDir,onLog);
197240
if(!deployed.onListMemoryRecords)return{success: false,error: 'No deployed AgentCore Memory found'};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import{handleGetCloudWatchTrace,handleListCloudWatchTraces}from'../handlers/cloudwatch-traces.js';
2+
importtype{RouteContext}from'../handlers/route-context.js';
3+
importtype{IncomingMessage,ServerResponse}from'http';
4+
import{beforeEach,describe,expect,it,vi}from'vitest';
5+
6+
functionmockRes(): ServerResponse&{_status: number;_headers: Record<string,string>;_body: string}{
7+
constres={
8+
_status: 0,
9+
_headers: {}asRecord<string,string>,
10+
_body: '',
11+
writeHead(status: number,headers?: Record<string,string>){
12+
res._status=status;
13+
if(headers)Object.assign(res._headers,headers);
14+
returnres;
15+
},
16+
setHeader(name: string,value: string){
17+
res._headers[name]=value;
18+
},
19+
end(body?: string){
20+
if(body)res._body=body;
21+
},
22+
};
23+
returnresasunknownasServerResponse&{_status: number;_headers: Record<string,string>;_body: string};
24+
}
25+
26+
functionmockReq(url: string): IncomingMessage{
27+
return{ url,headers: {host: 'localhost:8081'}}asunknownasIncomingMessage;
28+
}
29+
30+
functionmockCtx(overrides: Partial<RouteContext['options']>={}): RouteContext{
31+
return{
32+
options: {
33+
mode: 'dev',
34+
agents: [],
35+
harnesses: [],
36+
uiPort: 8081,
37+
...overrides,
38+
},
39+
runningAgents: newMap(),
40+
startingAgents: newMap(),
41+
agentErrors: newMap(),
42+
setCorsHeaders: vi.fn(),
43+
readBody: vi.fn(),
44+
}asRouteContext;
45+
}
46+
47+
describe('handleListCloudWatchTraces',()=>{
48+
beforeEach(()=>{
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns 404 when no handler configured',async()=>{
53+
constctx=mockCtx();
54+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
55+
constres=mockRes();
56+
57+
awaithandleListCloudWatchTraces(ctx,req,res);
58+
59+
expect(res._status).toBe(404);
60+
constbody=JSON.parse(res._body);
61+
expect(body.success).toBe(false);
62+
expect(body.error).toContain('not available');
63+
});
64+
65+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
66+
consthandler=vi.fn();
67+
constctx=mockCtx({onListCloudWatchTraces: handler});
68+
constreq=mockReq('/api/cloudwatch-traces');
69+
constres=mockRes();
70+
71+
awaithandleListCloudWatchTraces(ctx,req,res);
72+
73+
expect(res._status).toBe(400);
74+
constbody=JSON.parse(res._body);
75+
expect(body.success).toBe(false);
76+
expect(body.error).toContain('agentName');
77+
expect(body.error).toContain('harnessName');
78+
expect(handler).not.toHaveBeenCalled();
79+
});
80+
81+
it('returns 400 when both agentName and harnessName provided',async()=>{
82+
consthandler=vi.fn();
83+
constctx=mockCtx({onListCloudWatchTraces: handler});
84+
constreq=mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h');
85+
constres=mockRes();
86+
87+
awaithandleListCloudWatchTraces(ctx,req,res);
88+
89+
expect(res._status).toBe(400);
90+
constbody=JSON.parse(res._body);
91+
expect(body.success).toBe(false);
92+
expect(body.error).toContain('agentName');
93+
expect(body.error).toContain('harnessName');
94+
expect(handler).not.toHaveBeenCalled();
95+
});
96+
97+
it('calls handler with agentName and returns traces',async()=>{
98+
consttraces=[{traceId: 't1'},{traceId: 't2'}];
99+
consthandler=vi.fn().mockResolvedValue({success: true, traces });
100+
constctx=mockCtx({onListCloudWatchTraces: handler});
101+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
102+
constres=mockRes();
103+
104+
awaithandleListCloudWatchTraces(ctx,req,res);
105+
106+
expect(res._status).toBe(200);
107+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,undefined,undefined);
108+
constbody=JSON.parse(res._body);
109+
expect(body.success).toBe(true);
110+
expect(body.traces).toEqual(traces);
111+
});
112+
113+
it('calls handler with harnessName',async()=>{
114+
consthandler=vi.fn().mockResolvedValue({success: true,traces: []});
115+
constctx=mockCtx({onListCloudWatchTraces: handler});
116+
constreq=mockReq('/api/cloudwatch-traces?harnessName=my-harness');
117+
constres=mockRes();
118+
119+
awaithandleListCloudWatchTraces(ctx,req,res);
120+
121+
expect(res._status).toBe(200);
122+
expect(handler).toHaveBeenCalledWith(undefined,'my-harness',undefined,undefined);
123+
});
124+
125+
it('returns 500 when handler throws',async()=>{
126+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
127+
constctx=mockCtx({onListCloudWatchTraces: handler});
128+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
129+
constres=mockRes();
130+
131+
awaithandleListCloudWatchTraces(ctx,req,res);
132+
133+
expect(res._status).toBe(500);
134+
constbody=JSON.parse(res._body);
135+
expect(body.success).toBe(false);
136+
expect(body.error).toContain('Failed to list CloudWatch traces');
137+
});
138+
139+
it('returns 400 for invalid startTime',async()=>{
140+
consthandler=vi.fn();
141+
constctx=mockCtx({onListCloudWatchTraces: handler});
142+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber');
143+
constres=mockRes();
144+
145+
awaithandleListCloudWatchTraces(ctx,req,res);
146+
147+
expect(res._status).toBe(400);
148+
constbody=JSON.parse(res._body);
149+
expect(body.success).toBe(false);
150+
expect(body.error).toContain('startTime');
151+
expect(handler).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('handleGetCloudWatchTrace',()=>{
156+
beforeEach(()=>{
157+
vi.restoreAllMocks();
158+
});
159+
160+
it('returns 404 when no handler configured',async()=>{
161+
constctx=mockCtx();
162+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
163+
constres=mockRes();
164+
165+
awaithandleGetCloudWatchTrace(ctx,req,res);
166+
167+
expect(res._status).toBe(404);
168+
constbody=JSON.parse(res._body);
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain('not available');
171+
});
172+
173+
it('returns 400 when traceId is missing',async()=>{
174+
consthandler=vi.fn();
175+
constctx=mockCtx({onGetCloudWatchTrace: handler});
176+
constreq=mockReq('/api/cloudwatch-traces/?agentName=my-agent');
177+
constres=mockRes();
178+
179+
awaithandleGetCloudWatchTrace(ctx,req,res);
180+
181+
expect(res._status).toBe(400);
182+
constbody=JSON.parse(res._body);
183+
expect(body.success).toBe(false);
184+
expect(body.error).toContain('traceId');
185+
expect(handler).not.toHaveBeenCalled();
186+
});
187+
188+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
189+
consthandler=vi.fn();
190+
constctx=mockCtx({onGetCloudWatchTrace: handler});
191+
constreq=mockReq('/api/cloudwatch-traces/abc123');
192+
constres=mockRes();
193+
194+
awaithandleGetCloudWatchTrace(ctx,req,res);
195+
196+
expect(res._status).toBe(400);
197+
constbody=JSON.parse(res._body);
198+
expect(body.success).toBe(false);
199+
expect(body.error).toContain('agentName');
200+
expect(body.error).toContain('harnessName');
201+
expect(handler).not.toHaveBeenCalled();
202+
});
203+
204+
it('returns 500 when handler throws',async()=>{
205+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
206+
constctx=mockCtx({onGetCloudWatchTrace: handler});
207+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
208+
constres=mockRes();
209+
210+
awaithandleGetCloudWatchTrace(ctx,req,res);
211+
212+
expect(res._status).toBe(500);
213+
constbody=JSON.parse(res._body);
214+
expect(body.success).toBe(false);
215+
expect(body.error).toContain('Failed to get CloudWatch trace');
216+
});
217+
218+
it('calls handler and returns records',async()=>{
219+
constrecords=[{record: 'data1'}];
220+
consthandler=vi.fn().mockResolvedValue({success: true, records });
221+
constctx=mockCtx({onGetCloudWatchTrace: handler});
222+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
223+
constres=mockRes();
224+
225+
awaithandleGetCloudWatchTrace(ctx,req,res);
226+
227+
expect(res._status).toBe(200);
228+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,'abc123',undefined,undefined);
229+
constbody=JSON.parse(res._body);
230+
expect(body.success).toBe(true);
231+
expect(body.records).toEqual(records);
232+
});
233+
});

‎src/cli/operations/dev/web-ui/api-types.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* TODO: Extract these types into a shared package so both repos import
99
* from a single source of truth instead of manually duplicating.
1010
*/
11+
importtype{CloudWatchSpanRecord,CloudWatchTraceRecord}from'../../traces/types';
1112

1213
// ---------------------------------------------------------------------------
1314
// GET /api/status
@@ -279,6 +280,39 @@ export interface GetTraceResponse {
279280
error?: string;
280281
}
281282

283+
// ---------------------------------------------------------------------------
284+
// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx
285+
// ---------------------------------------------------------------------------
286+
287+
/** A single trace entry returned by the CloudWatch traces list endpoint */
288+
exportinterfaceCloudWatchTraceEntry{
289+
traceId: string;
290+
timestamp: string;
291+
sessionId?: string;
292+
spanCount?: string;
293+
}
294+
295+
/** Response shape for GET /api/cloudwatch-traces */
296+
exportinterfaceListCloudWatchTracesResponse{
297+
success: boolean;
298+
traces?: CloudWatchTraceEntry[];
299+
error?: string;
300+
}
301+
302+
// ---------------------------------------------------------------------------
303+
// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx
304+
// ---------------------------------------------------------------------------
305+
306+
/** Response shape for GET /api/cloudwatch-traces/:traceId */
307+
exportinterfaceGetCloudWatchTraceResponse{
308+
success: boolean;
309+
records?: CloudWatchTraceRecord[];
310+
spans?: CloudWatchSpanRecord[];
311+
error?: string;
312+
}
313+
314+
exporttype{CloudWatchTraceRecord,CloudWatchSpanRecord}from'../../traces/types';
315+
282316
// ---------------------------------------------------------------------------
283317
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
284318
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 76b07aa

Browse files
authored
feat: add CloudWatch traces API for web UI (#997)
1 parent 29b6522 commit 76b07aa

14 files changed

Lines changed: 1244 additions & 197 deletions

File tree

‎src/cli/commands/dev/browser-mode.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
runWebUI,
1010
}from'../../operations/dev/web-ui';
1111
import{listMemoryRecords,retrieveMemoryRecords}from'../../operations/memory';
12+
import{loadDeployedProjectConfig,resolveAgent}from'../../operations/resolve-agent';
13+
import{fetchTraceRecords,listTraces}from'../../operations/traces';
1214
importpathfrom'node:path';
1315

1416
interfaceDeployedHandlers{
@@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
192194
? (agentNameParam,startTime,endTime)=>collector.listTraces(agentNameParam,startTime,endTime)
193195
: undefined,
194196
onGetTrace: collector ? (agentNameParam,traceId)=>collector.getTraceSpans(agentNameParam,traceId) : undefined,
197+
onListCloudWatchTraces: async(agentName,_harnessName,startTime,endTime)=>{
198+
try{
199+
constconfigIO=newConfigIO({ baseDir });
200+
constcontext=awaitloadDeployedProjectConfig(configIO);
201+
constresolved=resolveAgent(context,{runtime: agentName});
202+
if(!resolved.success)return{success: false,error: resolved.error};
203+
returnlistTraces({
204+
region: resolved.agent.region,
205+
runtimeId: resolved.agent.runtimeId,
206+
agentName: resolved.agent.agentName,
207+
startTime,
208+
endTime,
209+
});
210+
}catch(err){
211+
return{
212+
success: false,
213+
error: `Failed to list CloudWatch traces: ${errinstanceofError ? err.message : String(err)}`,
214+
};
215+
}
216+
},
217+
onGetCloudWatchTrace: async(agentName,_harnessName,traceId,startTime,endTime)=>{
218+
try{
219+
constconfigIO=newConfigIO({ baseDir });
220+
constcontext=awaitloadDeployedProjectConfig(configIO);
221+
constresolved=resolveAgent(context,{runtime: agentName});
222+
if(!resolved.success)return{success: false,error: resolved.error};
223+
returnfetchTraceRecords({
224+
region: resolved.agent.region,
225+
runtimeId: resolved.agent.runtimeId,
226+
traceId,
227+
startTime,
228+
endTime,
229+
includeSpans: true,
230+
});
231+
}catch(err){
232+
return{
233+
success: false,
234+
error: `Failed to get CloudWatch trace: ${errinstanceofError ? err.message : String(err)}`,
235+
};
236+
}
237+
},
195238
onListMemoryRecords: async(memoryName,namespace,strategyId)=>{
196239
constdeployed=awaitresolveDeployedHandlers(baseDir,onLog);
197240
if(!deployed.onListMemoryRecords)return{success: false,error: 'No deployed AgentCore Memory found'};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import{handleGetCloudWatchTrace,handleListCloudWatchTraces}from'../handlers/cloudwatch-traces.js';
2+
importtype{RouteContext}from'../handlers/route-context.js';
3+
importtype{IncomingMessage,ServerResponse}from'http';
4+
import{beforeEach,describe,expect,it,vi}from'vitest';
5+
6+
functionmockRes(): ServerResponse&{_status: number;_headers: Record<string,string>;_body: string}{
7+
constres={
8+
_status: 0,
9+
_headers: {}asRecord<string,string>,
10+
_body: '',
11+
writeHead(status: number,headers?: Record<string,string>){
12+
res._status=status;
13+
if(headers)Object.assign(res._headers,headers);
14+
returnres;
15+
},
16+
setHeader(name: string,value: string){
17+
res._headers[name]=value;
18+
},
19+
end(body?: string){
20+
if(body)res._body=body;
21+
},
22+
};
23+
returnresasunknownasServerResponse&{_status: number;_headers: Record<string,string>;_body: string};
24+
}
25+
26+
functionmockReq(url: string): IncomingMessage{
27+
return{ url,headers: {host: 'localhost:8081'}}asunknownasIncomingMessage;
28+
}
29+
30+
functionmockCtx(overrides: Partial<RouteContext['options']>={}): RouteContext{
31+
return{
32+
options: {
33+
mode: 'dev',
34+
agents: [],
35+
harnesses: [],
36+
uiPort: 8081,
37+
...overrides,
38+
},
39+
runningAgents: newMap(),
40+
startingAgents: newMap(),
41+
agentErrors: newMap(),
42+
setCorsHeaders: vi.fn(),
43+
readBody: vi.fn(),
44+
}asRouteContext;
45+
}
46+
47+
describe('handleListCloudWatchTraces',()=>{
48+
beforeEach(()=>{
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns 404 when no handler configured',async()=>{
53+
constctx=mockCtx();
54+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
55+
constres=mockRes();
56+
57+
awaithandleListCloudWatchTraces(ctx,req,res);
58+
59+
expect(res._status).toBe(404);
60+
constbody=JSON.parse(res._body);
61+
expect(body.success).toBe(false);
62+
expect(body.error).toContain('not available');
63+
});
64+
65+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
66+
consthandler=vi.fn();
67+
constctx=mockCtx({onListCloudWatchTraces: handler});
68+
constreq=mockReq('/api/cloudwatch-traces');
69+
constres=mockRes();
70+
71+
awaithandleListCloudWatchTraces(ctx,req,res);
72+
73+
expect(res._status).toBe(400);
74+
constbody=JSON.parse(res._body);
75+
expect(body.success).toBe(false);
76+
expect(body.error).toContain('agentName');
77+
expect(body.error).toContain('harnessName');
78+
expect(handler).not.toHaveBeenCalled();
79+
});
80+
81+
it('returns 400 when both agentName and harnessName provided',async()=>{
82+
consthandler=vi.fn();
83+
constctx=mockCtx({onListCloudWatchTraces: handler});
84+
constreq=mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h');
85+
constres=mockRes();
86+
87+
awaithandleListCloudWatchTraces(ctx,req,res);
88+
89+
expect(res._status).toBe(400);
90+
constbody=JSON.parse(res._body);
91+
expect(body.success).toBe(false);
92+
expect(body.error).toContain('agentName');
93+
expect(body.error).toContain('harnessName');
94+
expect(handler).not.toHaveBeenCalled();
95+
});
96+
97+
it('calls handler with agentName and returns traces',async()=>{
98+
consttraces=[{traceId: 't1'},{traceId: 't2'}];
99+
consthandler=vi.fn().mockResolvedValue({success: true, traces });
100+
constctx=mockCtx({onListCloudWatchTraces: handler});
101+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
102+
constres=mockRes();
103+
104+
awaithandleListCloudWatchTraces(ctx,req,res);
105+
106+
expect(res._status).toBe(200);
107+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,undefined,undefined);
108+
constbody=JSON.parse(res._body);
109+
expect(body.success).toBe(true);
110+
expect(body.traces).toEqual(traces);
111+
});
112+
113+
it('calls handler with harnessName',async()=>{
114+
consthandler=vi.fn().mockResolvedValue({success: true,traces: []});
115+
constctx=mockCtx({onListCloudWatchTraces: handler});
116+
constreq=mockReq('/api/cloudwatch-traces?harnessName=my-harness');
117+
constres=mockRes();
118+
119+
awaithandleListCloudWatchTraces(ctx,req,res);
120+
121+
expect(res._status).toBe(200);
122+
expect(handler).toHaveBeenCalledWith(undefined,'my-harness',undefined,undefined);
123+
});
124+
125+
it('returns 500 when handler throws',async()=>{
126+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
127+
constctx=mockCtx({onListCloudWatchTraces: handler});
128+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
129+
constres=mockRes();
130+
131+
awaithandleListCloudWatchTraces(ctx,req,res);
132+
133+
expect(res._status).toBe(500);
134+
constbody=JSON.parse(res._body);
135+
expect(body.success).toBe(false);
136+
expect(body.error).toContain('Failed to list CloudWatch traces');
137+
});
138+
139+
it('returns 400 for invalid startTime',async()=>{
140+
consthandler=vi.fn();
141+
constctx=mockCtx({onListCloudWatchTraces: handler});
142+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber');
143+
constres=mockRes();
144+
145+
awaithandleListCloudWatchTraces(ctx,req,res);
146+
147+
expect(res._status).toBe(400);
148+
constbody=JSON.parse(res._body);
149+
expect(body.success).toBe(false);
150+
expect(body.error).toContain('startTime');
151+
expect(handler).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('handleGetCloudWatchTrace',()=>{
156+
beforeEach(()=>{
157+
vi.restoreAllMocks();
158+
});
159+
160+
it('returns 404 when no handler configured',async()=>{
161+
constctx=mockCtx();
162+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
163+
constres=mockRes();
164+
165+
awaithandleGetCloudWatchTrace(ctx,req,res);
166+
167+
expect(res._status).toBe(404);
168+
constbody=JSON.parse(res._body);
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain('not available');
171+
});
172+
173+
it('returns 400 when traceId is missing',async()=>{
174+
consthandler=vi.fn();
175+
constctx=mockCtx({onGetCloudWatchTrace: handler});
176+
constreq=mockReq('/api/cloudwatch-traces/?agentName=my-agent');
177+
constres=mockRes();
178+
179+
awaithandleGetCloudWatchTrace(ctx,req,res);
180+
181+
expect(res._status).toBe(400);
182+
constbody=JSON.parse(res._body);
183+
expect(body.success).toBe(false);
184+
expect(body.error).toContain('traceId');
185+
expect(handler).not.toHaveBeenCalled();
186+
});
187+
188+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
189+
consthandler=vi.fn();
190+
constctx=mockCtx({onGetCloudWatchTrace: handler});
191+
constreq=mockReq('/api/cloudwatch-traces/abc123');
192+
constres=mockRes();
193+
194+
awaithandleGetCloudWatchTrace(ctx,req,res);
195+
196+
expect(res._status).toBe(400);
197+
constbody=JSON.parse(res._body);
198+
expect(body.success).toBe(false);
199+
expect(body.error).toContain('agentName');
200+
expect(body.error).toContain('harnessName');
201+
expect(handler).not.toHaveBeenCalled();
202+
});
203+
204+
it('returns 500 when handler throws',async()=>{
205+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
206+
constctx=mockCtx({onGetCloudWatchTrace: handler});
207+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
208+
constres=mockRes();
209+
210+
awaithandleGetCloudWatchTrace(ctx,req,res);
211+
212+
expect(res._status).toBe(500);
213+
constbody=JSON.parse(res._body);
214+
expect(body.success).toBe(false);
215+
expect(body.error).toContain('Failed to get CloudWatch trace');
216+
});
217+
218+
it('calls handler and returns records',async()=>{
219+
constrecords=[{record: 'data1'}];
220+
consthandler=vi.fn().mockResolvedValue({success: true, records });
221+
constctx=mockCtx({onGetCloudWatchTrace: handler});
222+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
223+
constres=mockRes();
224+
225+
awaithandleGetCloudWatchTrace(ctx,req,res);
226+
227+
expect(res._status).toBe(200);
228+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,'abc123',undefined,undefined);
229+
constbody=JSON.parse(res._body);
230+
expect(body.success).toBe(true);
231+
expect(body.records).toEqual(records);
232+
});
233+
});

‎src/cli/operations/dev/web-ui/api-types.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* TODO: Extract these types into a shared package so both repos import
99
* from a single source of truth instead of manually duplicating.
1010
*/
11+
importtype{CloudWatchSpanRecord,CloudWatchTraceRecord}from'../../traces/types';
1112

1213
// ---------------------------------------------------------------------------
1314
// GET /api/status
@@ -279,6 +280,39 @@ export interface GetTraceResponse {
279280
error?: string;
280281
}
281282

283+
// ---------------------------------------------------------------------------
284+
// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx
285+
// ---------------------------------------------------------------------------
286+
287+
/** A single trace entry returned by the CloudWatch traces list endpoint */
288+
exportinterfaceCloudWatchTraceEntry{
289+
traceId: string;
290+
timestamp: string;
291+
sessionId?: string;
292+
spanCount?: string;
293+
}
294+
295+
/** Response shape for GET /api/cloudwatch-traces */
296+
exportinterfaceListCloudWatchTracesResponse{
297+
success: boolean;
298+
traces?: CloudWatchTraceEntry[];
299+
error?: string;
300+
}
301+
302+
// ---------------------------------------------------------------------------
303+
// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx
304+
// ---------------------------------------------------------------------------
305+
306+
/** Response shape for GET /api/cloudwatch-traces/:traceId */
307+
exportinterfaceGetCloudWatchTraceResponse{
308+
success: boolean;
309+
records?: CloudWatchTraceRecord[];
310+
spans?: CloudWatchSpanRecord[];
311+
error?: string;
312+
}
313+
314+
exporttype{CloudWatchTraceRecord,CloudWatchSpanRecord}from'../../traces/types';
315+
282316
// ---------------------------------------------------------------------------
283317
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
284318
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 76b07aa

Browse files
authored
feat: add CloudWatch traces API for web UI (#997)
1 parent 29b6522 commit 76b07aa

14 files changed

Lines changed: 1244 additions & 197 deletions

File tree

‎src/cli/commands/dev/browser-mode.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
runWebUI,
1010
}from'../../operations/dev/web-ui';
1111
import{listMemoryRecords,retrieveMemoryRecords}from'../../operations/memory';
12+
import{loadDeployedProjectConfig,resolveAgent}from'../../operations/resolve-agent';
13+
import{fetchTraceRecords,listTraces}from'../../operations/traces';
1214
importpathfrom'node:path';
1315

1416
interfaceDeployedHandlers{
@@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
192194
? (agentNameParam,startTime,endTime)=>collector.listTraces(agentNameParam,startTime,endTime)
193195
: undefined,
194196
onGetTrace: collector ? (agentNameParam,traceId)=>collector.getTraceSpans(agentNameParam,traceId) : undefined,
197+
onListCloudWatchTraces: async(agentName,_harnessName,startTime,endTime)=>{
198+
try{
199+
constconfigIO=newConfigIO({ baseDir });
200+
constcontext=awaitloadDeployedProjectConfig(configIO);
201+
constresolved=resolveAgent(context,{runtime: agentName});
202+
if(!resolved.success)return{success: false,error: resolved.error};
203+
returnlistTraces({
204+
region: resolved.agent.region,
205+
runtimeId: resolved.agent.runtimeId,
206+
agentName: resolved.agent.agentName,
207+
startTime,
208+
endTime,
209+
});
210+
}catch(err){
211+
return{
212+
success: false,
213+
error: `Failed to list CloudWatch traces: ${errinstanceofError ? err.message : String(err)}`,
214+
};
215+
}
216+
},
217+
onGetCloudWatchTrace: async(agentName,_harnessName,traceId,startTime,endTime)=>{
218+
try{
219+
constconfigIO=newConfigIO({ baseDir });
220+
constcontext=awaitloadDeployedProjectConfig(configIO);
221+
constresolved=resolveAgent(context,{runtime: agentName});
222+
if(!resolved.success)return{success: false,error: resolved.error};
223+
returnfetchTraceRecords({
224+
region: resolved.agent.region,
225+
runtimeId: resolved.agent.runtimeId,
226+
traceId,
227+
startTime,
228+
endTime,
229+
includeSpans: true,
230+
});
231+
}catch(err){
232+
return{
233+
success: false,
234+
error: `Failed to get CloudWatch trace: ${errinstanceofError ? err.message : String(err)}`,
235+
};
236+
}
237+
},
195238
onListMemoryRecords: async(memoryName,namespace,strategyId)=>{
196239
constdeployed=awaitresolveDeployedHandlers(baseDir,onLog);
197240
if(!deployed.onListMemoryRecords)return{success: false,error: 'No deployed AgentCore Memory found'};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import{handleGetCloudWatchTrace,handleListCloudWatchTraces}from'../handlers/cloudwatch-traces.js';
2+
importtype{RouteContext}from'../handlers/route-context.js';
3+
importtype{IncomingMessage,ServerResponse}from'http';
4+
import{beforeEach,describe,expect,it,vi}from'vitest';
5+
6+
functionmockRes(): ServerResponse&{_status: number;_headers: Record<string,string>;_body: string}{
7+
constres={
8+
_status: 0,
9+
_headers: {}asRecord<string,string>,
10+
_body: '',
11+
writeHead(status: number,headers?: Record<string,string>){
12+
res._status=status;
13+
if(headers)Object.assign(res._headers,headers);
14+
returnres;
15+
},
16+
setHeader(name: string,value: string){
17+
res._headers[name]=value;
18+
},
19+
end(body?: string){
20+
if(body)res._body=body;
21+
},
22+
};
23+
returnresasunknownasServerResponse&{_status: number;_headers: Record<string,string>;_body: string};
24+
}
25+
26+
functionmockReq(url: string): IncomingMessage{
27+
return{ url,headers: {host: 'localhost:8081'}}asunknownasIncomingMessage;
28+
}
29+
30+
functionmockCtx(overrides: Partial<RouteContext['options']>={}): RouteContext{
31+
return{
32+
options: {
33+
mode: 'dev',
34+
agents: [],
35+
harnesses: [],
36+
uiPort: 8081,
37+
...overrides,
38+
},
39+
runningAgents: newMap(),
40+
startingAgents: newMap(),
41+
agentErrors: newMap(),
42+
setCorsHeaders: vi.fn(),
43+
readBody: vi.fn(),
44+
}asRouteContext;
45+
}
46+
47+
describe('handleListCloudWatchTraces',()=>{
48+
beforeEach(()=>{
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns 404 when no handler configured',async()=>{
53+
constctx=mockCtx();
54+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
55+
constres=mockRes();
56+
57+
awaithandleListCloudWatchTraces(ctx,req,res);
58+
59+
expect(res._status).toBe(404);
60+
constbody=JSON.parse(res._body);
61+
expect(body.success).toBe(false);
62+
expect(body.error).toContain('not available');
63+
});
64+
65+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
66+
consthandler=vi.fn();
67+
constctx=mockCtx({onListCloudWatchTraces: handler});
68+
constreq=mockReq('/api/cloudwatch-traces');
69+
constres=mockRes();
70+
71+
awaithandleListCloudWatchTraces(ctx,req,res);
72+
73+
expect(res._status).toBe(400);
74+
constbody=JSON.parse(res._body);
75+
expect(body.success).toBe(false);
76+
expect(body.error).toContain('agentName');
77+
expect(body.error).toContain('harnessName');
78+
expect(handler).not.toHaveBeenCalled();
79+
});
80+
81+
it('returns 400 when both agentName and harnessName provided',async()=>{
82+
consthandler=vi.fn();
83+
constctx=mockCtx({onListCloudWatchTraces: handler});
84+
constreq=mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h');
85+
constres=mockRes();
86+
87+
awaithandleListCloudWatchTraces(ctx,req,res);
88+
89+
expect(res._status).toBe(400);
90+
constbody=JSON.parse(res._body);
91+
expect(body.success).toBe(false);
92+
expect(body.error).toContain('agentName');
93+
expect(body.error).toContain('harnessName');
94+
expect(handler).not.toHaveBeenCalled();
95+
});
96+
97+
it('calls handler with agentName and returns traces',async()=>{
98+
consttraces=[{traceId: 't1'},{traceId: 't2'}];
99+
consthandler=vi.fn().mockResolvedValue({success: true, traces });
100+
constctx=mockCtx({onListCloudWatchTraces: handler});
101+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
102+
constres=mockRes();
103+
104+
awaithandleListCloudWatchTraces(ctx,req,res);
105+
106+
expect(res._status).toBe(200);
107+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,undefined,undefined);
108+
constbody=JSON.parse(res._body);
109+
expect(body.success).toBe(true);
110+
expect(body.traces).toEqual(traces);
111+
});
112+
113+
it('calls handler with harnessName',async()=>{
114+
consthandler=vi.fn().mockResolvedValue({success: true,traces: []});
115+
constctx=mockCtx({onListCloudWatchTraces: handler});
116+
constreq=mockReq('/api/cloudwatch-traces?harnessName=my-harness');
117+
constres=mockRes();
118+
119+
awaithandleListCloudWatchTraces(ctx,req,res);
120+
121+
expect(res._status).toBe(200);
122+
expect(handler).toHaveBeenCalledWith(undefined,'my-harness',undefined,undefined);
123+
});
124+
125+
it('returns 500 when handler throws',async()=>{
126+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
127+
constctx=mockCtx({onListCloudWatchTraces: handler});
128+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
129+
constres=mockRes();
130+
131+
awaithandleListCloudWatchTraces(ctx,req,res);
132+
133+
expect(res._status).toBe(500);
134+
constbody=JSON.parse(res._body);
135+
expect(body.success).toBe(false);
136+
expect(body.error).toContain('Failed to list CloudWatch traces');
137+
});
138+
139+
it('returns 400 for invalid startTime',async()=>{
140+
consthandler=vi.fn();
141+
constctx=mockCtx({onListCloudWatchTraces: handler});
142+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber');
143+
constres=mockRes();
144+
145+
awaithandleListCloudWatchTraces(ctx,req,res);
146+
147+
expect(res._status).toBe(400);
148+
constbody=JSON.parse(res._body);
149+
expect(body.success).toBe(false);
150+
expect(body.error).toContain('startTime');
151+
expect(handler).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('handleGetCloudWatchTrace',()=>{
156+
beforeEach(()=>{
157+
vi.restoreAllMocks();
158+
});
159+
160+
it('returns 404 when no handler configured',async()=>{
161+
constctx=mockCtx();
162+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
163+
constres=mockRes();
164+
165+
awaithandleGetCloudWatchTrace(ctx,req,res);
166+
167+
expect(res._status).toBe(404);
168+
constbody=JSON.parse(res._body);
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain('not available');
171+
});
172+
173+
it('returns 400 when traceId is missing',async()=>{
174+
consthandler=vi.fn();
175+
constctx=mockCtx({onGetCloudWatchTrace: handler});
176+
constreq=mockReq('/api/cloudwatch-traces/?agentName=my-agent');
177+
constres=mockRes();
178+
179+
awaithandleGetCloudWatchTrace(ctx,req,res);
180+
181+
expect(res._status).toBe(400);
182+
constbody=JSON.parse(res._body);
183+
expect(body.success).toBe(false);
184+
expect(body.error).toContain('traceId');
185+
expect(handler).not.toHaveBeenCalled();
186+
});
187+
188+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
189+
consthandler=vi.fn();
190+
constctx=mockCtx({onGetCloudWatchTrace: handler});
191+
constreq=mockReq('/api/cloudwatch-traces/abc123');
192+
constres=mockRes();
193+
194+
awaithandleGetCloudWatchTrace(ctx,req,res);
195+
196+
expect(res._status).toBe(400);
197+
constbody=JSON.parse(res._body);
198+
expect(body.success).toBe(false);
199+
expect(body.error).toContain('agentName');
200+
expect(body.error).toContain('harnessName');
201+
expect(handler).not.toHaveBeenCalled();
202+
});
203+
204+
it('returns 500 when handler throws',async()=>{
205+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
206+
constctx=mockCtx({onGetCloudWatchTrace: handler});
207+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
208+
constres=mockRes();
209+
210+
awaithandleGetCloudWatchTrace(ctx,req,res);
211+
212+
expect(res._status).toBe(500);
213+
constbody=JSON.parse(res._body);
214+
expect(body.success).toBe(false);
215+
expect(body.error).toContain('Failed to get CloudWatch trace');
216+
});
217+
218+
it('calls handler and returns records',async()=>{
219+
constrecords=[{record: 'data1'}];
220+
consthandler=vi.fn().mockResolvedValue({success: true, records });
221+
constctx=mockCtx({onGetCloudWatchTrace: handler});
222+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
223+
constres=mockRes();
224+
225+
awaithandleGetCloudWatchTrace(ctx,req,res);
226+
227+
expect(res._status).toBe(200);
228+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,'abc123',undefined,undefined);
229+
constbody=JSON.parse(res._body);
230+
expect(body.success).toBe(true);
231+
expect(body.records).toEqual(records);
232+
});
233+
});

‎src/cli/operations/dev/web-ui/api-types.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* TODO: Extract these types into a shared package so both repos import
99
* from a single source of truth instead of manually duplicating.
1010
*/
11+
importtype{CloudWatchSpanRecord,CloudWatchTraceRecord}from'../../traces/types';
1112

1213
// ---------------------------------------------------------------------------
1314
// GET /api/status
@@ -279,6 +280,39 @@ export interface GetTraceResponse {
279280
error?: string;
280281
}
281282

283+
// ---------------------------------------------------------------------------
284+
// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx
285+
// ---------------------------------------------------------------------------
286+
287+
/** A single trace entry returned by the CloudWatch traces list endpoint */
288+
exportinterfaceCloudWatchTraceEntry{
289+
traceId: string;
290+
timestamp: string;
291+
sessionId?: string;
292+
spanCount?: string;
293+
}
294+
295+
/** Response shape for GET /api/cloudwatch-traces */
296+
exportinterfaceListCloudWatchTracesResponse{
297+
success: boolean;
298+
traces?: CloudWatchTraceEntry[];
299+
error?: string;
300+
}
301+
302+
// ---------------------------------------------------------------------------
303+
// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx
304+
// ---------------------------------------------------------------------------
305+
306+
/** Response shape for GET /api/cloudwatch-traces/:traceId */
307+
exportinterfaceGetCloudWatchTraceResponse{
308+
success: boolean;
309+
records?: CloudWatchTraceRecord[];
310+
spans?: CloudWatchSpanRecord[];
311+
error?: string;
312+
}
313+
314+
exporttype{CloudWatchTraceRecord,CloudWatchSpanRecord}from'../../traces/types';
315+
282316
// ---------------------------------------------------------------------------
283317
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
284318
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 76b07aa

Browse files
authored
feat: add CloudWatch traces API for web UI (#997)
1 parent 29b6522 commit 76b07aa

14 files changed

Lines changed: 1244 additions & 197 deletions

File tree

‎src/cli/commands/dev/browser-mode.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
runWebUI,
1010
}from'../../operations/dev/web-ui';
1111
import{listMemoryRecords,retrieveMemoryRecords}from'../../operations/memory';
12+
import{loadDeployedProjectConfig,resolveAgent}from'../../operations/resolve-agent';
13+
import{fetchTraceRecords,listTraces}from'../../operations/traces';
1214
importpathfrom'node:path';
1315

1416
interfaceDeployedHandlers{
@@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
192194
? (agentNameParam,startTime,endTime)=>collector.listTraces(agentNameParam,startTime,endTime)
193195
: undefined,
194196
onGetTrace: collector ? (agentNameParam,traceId)=>collector.getTraceSpans(agentNameParam,traceId) : undefined,
197+
onListCloudWatchTraces: async(agentName,_harnessName,startTime,endTime)=>{
198+
try{
199+
constconfigIO=newConfigIO({ baseDir });
200+
constcontext=awaitloadDeployedProjectConfig(configIO);
201+
constresolved=resolveAgent(context,{runtime: agentName});
202+
if(!resolved.success)return{success: false,error: resolved.error};
203+
returnlistTraces({
204+
region: resolved.agent.region,
205+
runtimeId: resolved.agent.runtimeId,
206+
agentName: resolved.agent.agentName,
207+
startTime,
208+
endTime,
209+
});
210+
}catch(err){
211+
return{
212+
success: false,
213+
error: `Failed to list CloudWatch traces: ${errinstanceofError ? err.message : String(err)}`,
214+
};
215+
}
216+
},
217+
onGetCloudWatchTrace: async(agentName,_harnessName,traceId,startTime,endTime)=>{
218+
try{
219+
constconfigIO=newConfigIO({ baseDir });
220+
constcontext=awaitloadDeployedProjectConfig(configIO);
221+
constresolved=resolveAgent(context,{runtime: agentName});
222+
if(!resolved.success)return{success: false,error: resolved.error};
223+
returnfetchTraceRecords({
224+
region: resolved.agent.region,
225+
runtimeId: resolved.agent.runtimeId,
226+
traceId,
227+
startTime,
228+
endTime,
229+
includeSpans: true,
230+
});
231+
}catch(err){
232+
return{
233+
success: false,
234+
error: `Failed to get CloudWatch trace: ${errinstanceofError ? err.message : String(err)}`,
235+
};
236+
}
237+
},
195238
onListMemoryRecords: async(memoryName,namespace,strategyId)=>{
196239
constdeployed=awaitresolveDeployedHandlers(baseDir,onLog);
197240
if(!deployed.onListMemoryRecords)return{success: false,error: 'No deployed AgentCore Memory found'};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import{handleGetCloudWatchTrace,handleListCloudWatchTraces}from'../handlers/cloudwatch-traces.js';
2+
importtype{RouteContext}from'../handlers/route-context.js';
3+
importtype{IncomingMessage,ServerResponse}from'http';
4+
import{beforeEach,describe,expect,it,vi}from'vitest';
5+
6+
functionmockRes(): ServerResponse&{_status: number;_headers: Record<string,string>;_body: string}{
7+
constres={
8+
_status: 0,
9+
_headers: {}asRecord<string,string>,
10+
_body: '',
11+
writeHead(status: number,headers?: Record<string,string>){
12+
res._status=status;
13+
if(headers)Object.assign(res._headers,headers);
14+
returnres;
15+
},
16+
setHeader(name: string,value: string){
17+
res._headers[name]=value;
18+
},
19+
end(body?: string){
20+
if(body)res._body=body;
21+
},
22+
};
23+
returnresasunknownasServerResponse&{_status: number;_headers: Record<string,string>;_body: string};
24+
}
25+
26+
functionmockReq(url: string): IncomingMessage{
27+
return{ url,headers: {host: 'localhost:8081'}}asunknownasIncomingMessage;
28+
}
29+
30+
functionmockCtx(overrides: Partial<RouteContext['options']>={}): RouteContext{
31+
return{
32+
options: {
33+
mode: 'dev',
34+
agents: [],
35+
harnesses: [],
36+
uiPort: 8081,
37+
...overrides,
38+
},
39+
runningAgents: newMap(),
40+
startingAgents: newMap(),
41+
agentErrors: newMap(),
42+
setCorsHeaders: vi.fn(),
43+
readBody: vi.fn(),
44+
}asRouteContext;
45+
}
46+
47+
describe('handleListCloudWatchTraces',()=>{
48+
beforeEach(()=>{
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns 404 when no handler configured',async()=>{
53+
constctx=mockCtx();
54+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
55+
constres=mockRes();
56+
57+
awaithandleListCloudWatchTraces(ctx,req,res);
58+
59+
expect(res._status).toBe(404);
60+
constbody=JSON.parse(res._body);
61+
expect(body.success).toBe(false);
62+
expect(body.error).toContain('not available');
63+
});
64+
65+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
66+
consthandler=vi.fn();
67+
constctx=mockCtx({onListCloudWatchTraces: handler});
68+
constreq=mockReq('/api/cloudwatch-traces');
69+
constres=mockRes();
70+
71+
awaithandleListCloudWatchTraces(ctx,req,res);
72+
73+
expect(res._status).toBe(400);
74+
constbody=JSON.parse(res._body);
75+
expect(body.success).toBe(false);
76+
expect(body.error).toContain('agentName');
77+
expect(body.error).toContain('harnessName');
78+
expect(handler).not.toHaveBeenCalled();
79+
});
80+
81+
it('returns 400 when both agentName and harnessName provided',async()=>{
82+
consthandler=vi.fn();
83+
constctx=mockCtx({onListCloudWatchTraces: handler});
84+
constreq=mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h');
85+
constres=mockRes();
86+
87+
awaithandleListCloudWatchTraces(ctx,req,res);
88+
89+
expect(res._status).toBe(400);
90+
constbody=JSON.parse(res._body);
91+
expect(body.success).toBe(false);
92+
expect(body.error).toContain('agentName');
93+
expect(body.error).toContain('harnessName');
94+
expect(handler).not.toHaveBeenCalled();
95+
});
96+
97+
it('calls handler with agentName and returns traces',async()=>{
98+
consttraces=[{traceId: 't1'},{traceId: 't2'}];
99+
consthandler=vi.fn().mockResolvedValue({success: true, traces });
100+
constctx=mockCtx({onListCloudWatchTraces: handler});
101+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
102+
constres=mockRes();
103+
104+
awaithandleListCloudWatchTraces(ctx,req,res);
105+
106+
expect(res._status).toBe(200);
107+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,undefined,undefined);
108+
constbody=JSON.parse(res._body);
109+
expect(body.success).toBe(true);
110+
expect(body.traces).toEqual(traces);
111+
});
112+
113+
it('calls handler with harnessName',async()=>{
114+
consthandler=vi.fn().mockResolvedValue({success: true,traces: []});
115+
constctx=mockCtx({onListCloudWatchTraces: handler});
116+
constreq=mockReq('/api/cloudwatch-traces?harnessName=my-harness');
117+
constres=mockRes();
118+
119+
awaithandleListCloudWatchTraces(ctx,req,res);
120+
121+
expect(res._status).toBe(200);
122+
expect(handler).toHaveBeenCalledWith(undefined,'my-harness',undefined,undefined);
123+
});
124+
125+
it('returns 500 when handler throws',async()=>{
126+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
127+
constctx=mockCtx({onListCloudWatchTraces: handler});
128+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
129+
constres=mockRes();
130+
131+
awaithandleListCloudWatchTraces(ctx,req,res);
132+
133+
expect(res._status).toBe(500);
134+
constbody=JSON.parse(res._body);
135+
expect(body.success).toBe(false);
136+
expect(body.error).toContain('Failed to list CloudWatch traces');
137+
});
138+
139+
it('returns 400 for invalid startTime',async()=>{
140+
consthandler=vi.fn();
141+
constctx=mockCtx({onListCloudWatchTraces: handler});
142+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber');
143+
constres=mockRes();
144+
145+
awaithandleListCloudWatchTraces(ctx,req,res);
146+
147+
expect(res._status).toBe(400);
148+
constbody=JSON.parse(res._body);
149+
expect(body.success).toBe(false);
150+
expect(body.error).toContain('startTime');
151+
expect(handler).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('handleGetCloudWatchTrace',()=>{
156+
beforeEach(()=>{
157+
vi.restoreAllMocks();
158+
});
159+
160+
it('returns 404 when no handler configured',async()=>{
161+
constctx=mockCtx();
162+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
163+
constres=mockRes();
164+
165+
awaithandleGetCloudWatchTrace(ctx,req,res);
166+
167+
expect(res._status).toBe(404);
168+
constbody=JSON.parse(res._body);
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain('not available');
171+
});
172+
173+
it('returns 400 when traceId is missing',async()=>{
174+
consthandler=vi.fn();
175+
constctx=mockCtx({onGetCloudWatchTrace: handler});
176+
constreq=mockReq('/api/cloudwatch-traces/?agentName=my-agent');
177+
constres=mockRes();
178+
179+
awaithandleGetCloudWatchTrace(ctx,req,res);
180+
181+
expect(res._status).toBe(400);
182+
constbody=JSON.parse(res._body);
183+
expect(body.success).toBe(false);
184+
expect(body.error).toContain('traceId');
185+
expect(handler).not.toHaveBeenCalled();
186+
});
187+
188+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
189+
consthandler=vi.fn();
190+
constctx=mockCtx({onGetCloudWatchTrace: handler});
191+
constreq=mockReq('/api/cloudwatch-traces/abc123');
192+
constres=mockRes();
193+
194+
awaithandleGetCloudWatchTrace(ctx,req,res);
195+
196+
expect(res._status).toBe(400);
197+
constbody=JSON.parse(res._body);
198+
expect(body.success).toBe(false);
199+
expect(body.error).toContain('agentName');
200+
expect(body.error).toContain('harnessName');
201+
expect(handler).not.toHaveBeenCalled();
202+
});
203+
204+
it('returns 500 when handler throws',async()=>{
205+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
206+
constctx=mockCtx({onGetCloudWatchTrace: handler});
207+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
208+
constres=mockRes();
209+
210+
awaithandleGetCloudWatchTrace(ctx,req,res);
211+
212+
expect(res._status).toBe(500);
213+
constbody=JSON.parse(res._body);
214+
expect(body.success).toBe(false);
215+
expect(body.error).toContain('Failed to get CloudWatch trace');
216+
});
217+
218+
it('calls handler and returns records',async()=>{
219+
constrecords=[{record: 'data1'}];
220+
consthandler=vi.fn().mockResolvedValue({success: true, records });
221+
constctx=mockCtx({onGetCloudWatchTrace: handler});
222+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
223+
constres=mockRes();
224+
225+
awaithandleGetCloudWatchTrace(ctx,req,res);
226+
227+
expect(res._status).toBe(200);
228+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,'abc123',undefined,undefined);
229+
constbody=JSON.parse(res._body);
230+
expect(body.success).toBe(true);
231+
expect(body.records).toEqual(records);
232+
});
233+
});

‎src/cli/operations/dev/web-ui/api-types.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* TODO: Extract these types into a shared package so both repos import
99
* from a single source of truth instead of manually duplicating.
1010
*/
11+
importtype{CloudWatchSpanRecord,CloudWatchTraceRecord}from'../../traces/types';
1112

1213
// ---------------------------------------------------------------------------
1314
// GET /api/status
@@ -279,6 +280,39 @@ export interface GetTraceResponse {
279280
error?: string;
280281
}
281282

283+
// ---------------------------------------------------------------------------
284+
// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx
285+
// ---------------------------------------------------------------------------
286+
287+
/** A single trace entry returned by the CloudWatch traces list endpoint */
288+
exportinterfaceCloudWatchTraceEntry{
289+
traceId: string;
290+
timestamp: string;
291+
sessionId?: string;
292+
spanCount?: string;
293+
}
294+
295+
/** Response shape for GET /api/cloudwatch-traces */
296+
exportinterfaceListCloudWatchTracesResponse{
297+
success: boolean;
298+
traces?: CloudWatchTraceEntry[];
299+
error?: string;
300+
}
301+
302+
// ---------------------------------------------------------------------------
303+
// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx
304+
// ---------------------------------------------------------------------------
305+
306+
/** Response shape for GET /api/cloudwatch-traces/:traceId */
307+
exportinterfaceGetCloudWatchTraceResponse{
308+
success: boolean;
309+
records?: CloudWatchTraceRecord[];
310+
spans?: CloudWatchSpanRecord[];
311+
error?: string;
312+
}
313+
314+
exporttype{CloudWatchTraceRecord,CloudWatchSpanRecord}from'../../traces/types';
315+
282316
// ---------------------------------------------------------------------------
283317
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
284318
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 76b07aa

Browse files
authored
feat: add CloudWatch traces API for web UI (#997)
1 parent 29b6522 commit 76b07aa

14 files changed

Lines changed: 1244 additions & 197 deletions

File tree

‎src/cli/commands/dev/browser-mode.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
runWebUI,
1010
}from'../../operations/dev/web-ui';
1111
import{listMemoryRecords,retrieveMemoryRecords}from'../../operations/memory';
12+
import{loadDeployedProjectConfig,resolveAgent}from'../../operations/resolve-agent';
13+
import{fetchTraceRecords,listTraces}from'../../operations/traces';
1214
importpathfrom'node:path';
1315

1416
interfaceDeployedHandlers{
@@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
192194
? (agentNameParam,startTime,endTime)=>collector.listTraces(agentNameParam,startTime,endTime)
193195
: undefined,
194196
onGetTrace: collector ? (agentNameParam,traceId)=>collector.getTraceSpans(agentNameParam,traceId) : undefined,
197+
onListCloudWatchTraces: async(agentName,_harnessName,startTime,endTime)=>{
198+
try{
199+
constconfigIO=newConfigIO({ baseDir });
200+
constcontext=awaitloadDeployedProjectConfig(configIO);
201+
constresolved=resolveAgent(context,{runtime: agentName});
202+
if(!resolved.success)return{success: false,error: resolved.error};
203+
returnlistTraces({
204+
region: resolved.agent.region,
205+
runtimeId: resolved.agent.runtimeId,
206+
agentName: resolved.agent.agentName,
207+
startTime,
208+
endTime,
209+
});
210+
}catch(err){
211+
return{
212+
success: false,
213+
error: `Failed to list CloudWatch traces: ${errinstanceofError ? err.message : String(err)}`,
214+
};
215+
}
216+
},
217+
onGetCloudWatchTrace: async(agentName,_harnessName,traceId,startTime,endTime)=>{
218+
try{
219+
constconfigIO=newConfigIO({ baseDir });
220+
constcontext=awaitloadDeployedProjectConfig(configIO);
221+
constresolved=resolveAgent(context,{runtime: agentName});
222+
if(!resolved.success)return{success: false,error: resolved.error};
223+
returnfetchTraceRecords({
224+
region: resolved.agent.region,
225+
runtimeId: resolved.agent.runtimeId,
226+
traceId,
227+
startTime,
228+
endTime,
229+
includeSpans: true,
230+
});
231+
}catch(err){
232+
return{
233+
success: false,
234+
error: `Failed to get CloudWatch trace: ${errinstanceofError ? err.message : String(err)}`,
235+
};
236+
}
237+
},
195238
onListMemoryRecords: async(memoryName,namespace,strategyId)=>{
196239
constdeployed=awaitresolveDeployedHandlers(baseDir,onLog);
197240
if(!deployed.onListMemoryRecords)return{success: false,error: 'No deployed AgentCore Memory found'};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import{handleGetCloudWatchTrace,handleListCloudWatchTraces}from'../handlers/cloudwatch-traces.js';
2+
importtype{RouteContext}from'../handlers/route-context.js';
3+
importtype{IncomingMessage,ServerResponse}from'http';
4+
import{beforeEach,describe,expect,it,vi}from'vitest';
5+
6+
functionmockRes(): ServerResponse&{_status: number;_headers: Record<string,string>;_body: string}{
7+
constres={
8+
_status: 0,
9+
_headers: {}asRecord<string,string>,
10+
_body: '',
11+
writeHead(status: number,headers?: Record<string,string>){
12+
res._status=status;
13+
if(headers)Object.assign(res._headers,headers);
14+
returnres;
15+
},
16+
setHeader(name: string,value: string){
17+
res._headers[name]=value;
18+
},
19+
end(body?: string){
20+
if(body)res._body=body;
21+
},
22+
};
23+
returnresasunknownasServerResponse&{_status: number;_headers: Record<string,string>;_body: string};
24+
}
25+
26+
functionmockReq(url: string): IncomingMessage{
27+
return{ url,headers: {host: 'localhost:8081'}}asunknownasIncomingMessage;
28+
}
29+
30+
functionmockCtx(overrides: Partial<RouteContext['options']>={}): RouteContext{
31+
return{
32+
options: {
33+
mode: 'dev',
34+
agents: [],
35+
harnesses: [],
36+
uiPort: 8081,
37+
...overrides,
38+
},
39+
runningAgents: newMap(),
40+
startingAgents: newMap(),
41+
agentErrors: newMap(),
42+
setCorsHeaders: vi.fn(),
43+
readBody: vi.fn(),
44+
}asRouteContext;
45+
}
46+
47+
describe('handleListCloudWatchTraces',()=>{
48+
beforeEach(()=>{
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns 404 when no handler configured',async()=>{
53+
constctx=mockCtx();
54+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
55+
constres=mockRes();
56+
57+
awaithandleListCloudWatchTraces(ctx,req,res);
58+
59+
expect(res._status).toBe(404);
60+
constbody=JSON.parse(res._body);
61+
expect(body.success).toBe(false);
62+
expect(body.error).toContain('not available');
63+
});
64+
65+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
66+
consthandler=vi.fn();
67+
constctx=mockCtx({onListCloudWatchTraces: handler});
68+
constreq=mockReq('/api/cloudwatch-traces');
69+
constres=mockRes();
70+
71+
awaithandleListCloudWatchTraces(ctx,req,res);
72+
73+
expect(res._status).toBe(400);
74+
constbody=JSON.parse(res._body);
75+
expect(body.success).toBe(false);
76+
expect(body.error).toContain('agentName');
77+
expect(body.error).toContain('harnessName');
78+
expect(handler).not.toHaveBeenCalled();
79+
});
80+
81+
it('returns 400 when both agentName and harnessName provided',async()=>{
82+
consthandler=vi.fn();
83+
constctx=mockCtx({onListCloudWatchTraces: handler});
84+
constreq=mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h');
85+
constres=mockRes();
86+
87+
awaithandleListCloudWatchTraces(ctx,req,res);
88+
89+
expect(res._status).toBe(400);
90+
constbody=JSON.parse(res._body);
91+
expect(body.success).toBe(false);
92+
expect(body.error).toContain('agentName');
93+
expect(body.error).toContain('harnessName');
94+
expect(handler).not.toHaveBeenCalled();
95+
});
96+
97+
it('calls handler with agentName and returns traces',async()=>{
98+
consttraces=[{traceId: 't1'},{traceId: 't2'}];
99+
consthandler=vi.fn().mockResolvedValue({success: true, traces });
100+
constctx=mockCtx({onListCloudWatchTraces: handler});
101+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
102+
constres=mockRes();
103+
104+
awaithandleListCloudWatchTraces(ctx,req,res);
105+
106+
expect(res._status).toBe(200);
107+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,undefined,undefined);
108+
constbody=JSON.parse(res._body);
109+
expect(body.success).toBe(true);
110+
expect(body.traces).toEqual(traces);
111+
});
112+
113+
it('calls handler with harnessName',async()=>{
114+
consthandler=vi.fn().mockResolvedValue({success: true,traces: []});
115+
constctx=mockCtx({onListCloudWatchTraces: handler});
116+
constreq=mockReq('/api/cloudwatch-traces?harnessName=my-harness');
117+
constres=mockRes();
118+
119+
awaithandleListCloudWatchTraces(ctx,req,res);
120+
121+
expect(res._status).toBe(200);
122+
expect(handler).toHaveBeenCalledWith(undefined,'my-harness',undefined,undefined);
123+
});
124+
125+
it('returns 500 when handler throws',async()=>{
126+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
127+
constctx=mockCtx({onListCloudWatchTraces: handler});
128+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
129+
constres=mockRes();
130+
131+
awaithandleListCloudWatchTraces(ctx,req,res);
132+
133+
expect(res._status).toBe(500);
134+
constbody=JSON.parse(res._body);
135+
expect(body.success).toBe(false);
136+
expect(body.error).toContain('Failed to list CloudWatch traces');
137+
});
138+
139+
it('returns 400 for invalid startTime',async()=>{
140+
consthandler=vi.fn();
141+
constctx=mockCtx({onListCloudWatchTraces: handler});
142+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber');
143+
constres=mockRes();
144+
145+
awaithandleListCloudWatchTraces(ctx,req,res);
146+
147+
expect(res._status).toBe(400);
148+
constbody=JSON.parse(res._body);
149+
expect(body.success).toBe(false);
150+
expect(body.error).toContain('startTime');
151+
expect(handler).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('handleGetCloudWatchTrace',()=>{
156+
beforeEach(()=>{
157+
vi.restoreAllMocks();
158+
});
159+
160+
it('returns 404 when no handler configured',async()=>{
161+
constctx=mockCtx();
162+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
163+
constres=mockRes();
164+
165+
awaithandleGetCloudWatchTrace(ctx,req,res);
166+
167+
expect(res._status).toBe(404);
168+
constbody=JSON.parse(res._body);
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain('not available');
171+
});
172+
173+
it('returns 400 when traceId is missing',async()=>{
174+
consthandler=vi.fn();
175+
constctx=mockCtx({onGetCloudWatchTrace: handler});
176+
constreq=mockReq('/api/cloudwatch-traces/?agentName=my-agent');
177+
constres=mockRes();
178+
179+
awaithandleGetCloudWatchTrace(ctx,req,res);
180+
181+
expect(res._status).toBe(400);
182+
constbody=JSON.parse(res._body);
183+
expect(body.success).toBe(false);
184+
expect(body.error).toContain('traceId');
185+
expect(handler).not.toHaveBeenCalled();
186+
});
187+
188+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
189+
consthandler=vi.fn();
190+
constctx=mockCtx({onGetCloudWatchTrace: handler});
191+
constreq=mockReq('/api/cloudwatch-traces/abc123');
192+
constres=mockRes();
193+
194+
awaithandleGetCloudWatchTrace(ctx,req,res);
195+
196+
expect(res._status).toBe(400);
197+
constbody=JSON.parse(res._body);
198+
expect(body.success).toBe(false);
199+
expect(body.error).toContain('agentName');
200+
expect(body.error).toContain('harnessName');
201+
expect(handler).not.toHaveBeenCalled();
202+
});
203+
204+
it('returns 500 when handler throws',async()=>{
205+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
206+
constctx=mockCtx({onGetCloudWatchTrace: handler});
207+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
208+
constres=mockRes();
209+
210+
awaithandleGetCloudWatchTrace(ctx,req,res);
211+
212+
expect(res._status).toBe(500);
213+
constbody=JSON.parse(res._body);
214+
expect(body.success).toBe(false);
215+
expect(body.error).toContain('Failed to get CloudWatch trace');
216+
});
217+
218+
it('calls handler and returns records',async()=>{
219+
constrecords=[{record: 'data1'}];
220+
consthandler=vi.fn().mockResolvedValue({success: true, records });
221+
constctx=mockCtx({onGetCloudWatchTrace: handler});
222+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
223+
constres=mockRes();
224+
225+
awaithandleGetCloudWatchTrace(ctx,req,res);
226+
227+
expect(res._status).toBe(200);
228+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,'abc123',undefined,undefined);
229+
constbody=JSON.parse(res._body);
230+
expect(body.success).toBe(true);
231+
expect(body.records).toEqual(records);
232+
});
233+
});

‎src/cli/operations/dev/web-ui/api-types.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* TODO: Extract these types into a shared package so both repos import
99
* from a single source of truth instead of manually duplicating.
1010
*/
11+
importtype{CloudWatchSpanRecord,CloudWatchTraceRecord}from'../../traces/types';
1112

1213
// ---------------------------------------------------------------------------
1314
// GET /api/status
@@ -279,6 +280,39 @@ export interface GetTraceResponse {
279280
error?: string;
280281
}
281282

283+
// ---------------------------------------------------------------------------
284+
// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx
285+
// ---------------------------------------------------------------------------
286+
287+
/** A single trace entry returned by the CloudWatch traces list endpoint */
288+
exportinterfaceCloudWatchTraceEntry{
289+
traceId: string;
290+
timestamp: string;
291+
sessionId?: string;
292+
spanCount?: string;
293+
}
294+
295+
/** Response shape for GET /api/cloudwatch-traces */
296+
exportinterfaceListCloudWatchTracesResponse{
297+
success: boolean;
298+
traces?: CloudWatchTraceEntry[];
299+
error?: string;
300+
}
301+
302+
// ---------------------------------------------------------------------------
303+
// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx
304+
// ---------------------------------------------------------------------------
305+
306+
/** Response shape for GET /api/cloudwatch-traces/:traceId */
307+
exportinterfaceGetCloudWatchTraceResponse{
308+
success: boolean;
309+
records?: CloudWatchTraceRecord[];
310+
spans?: CloudWatchSpanRecord[];
311+
error?: string;
312+
}
313+
314+
exporttype{CloudWatchTraceRecord,CloudWatchSpanRecord}from'../../traces/types';
315+
282316
// ---------------------------------------------------------------------------
283317
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
284318
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 76b07aa

Browse files
authored
feat: add CloudWatch traces API for web UI (#997)
1 parent 29b6522 commit 76b07aa

14 files changed

Lines changed: 1244 additions & 197 deletions

File tree

‎src/cli/commands/dev/browser-mode.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
runWebUI,
1010
}from'../../operations/dev/web-ui';
1111
import{listMemoryRecords,retrieveMemoryRecords}from'../../operations/memory';
12+
import{loadDeployedProjectConfig,resolveAgent}from'../../operations/resolve-agent';
13+
import{fetchTraceRecords,listTraces}from'../../operations/traces';
1214
importpathfrom'node:path';
1315

1416
interfaceDeployedHandlers{
@@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
192194
? (agentNameParam,startTime,endTime)=>collector.listTraces(agentNameParam,startTime,endTime)
193195
: undefined,
194196
onGetTrace: collector ? (agentNameParam,traceId)=>collector.getTraceSpans(agentNameParam,traceId) : undefined,
197+
onListCloudWatchTraces: async(agentName,_harnessName,startTime,endTime)=>{
198+
try{
199+
constconfigIO=newConfigIO({ baseDir });
200+
constcontext=awaitloadDeployedProjectConfig(configIO);
201+
constresolved=resolveAgent(context,{runtime: agentName});
202+
if(!resolved.success)return{success: false,error: resolved.error};
203+
returnlistTraces({
204+
region: resolved.agent.region,
205+
runtimeId: resolved.agent.runtimeId,
206+
agentName: resolved.agent.agentName,
207+
startTime,
208+
endTime,
209+
});
210+
}catch(err){
211+
return{
212+
success: false,
213+
error: `Failed to list CloudWatch traces: ${errinstanceofError ? err.message : String(err)}`,
214+
};
215+
}
216+
},
217+
onGetCloudWatchTrace: async(agentName,_harnessName,traceId,startTime,endTime)=>{
218+
try{
219+
constconfigIO=newConfigIO({ baseDir });
220+
constcontext=awaitloadDeployedProjectConfig(configIO);
221+
constresolved=resolveAgent(context,{runtime: agentName});
222+
if(!resolved.success)return{success: false,error: resolved.error};
223+
returnfetchTraceRecords({
224+
region: resolved.agent.region,
225+
runtimeId: resolved.agent.runtimeId,
226+
traceId,
227+
startTime,
228+
endTime,
229+
includeSpans: true,
230+
});
231+
}catch(err){
232+
return{
233+
success: false,
234+
error: `Failed to get CloudWatch trace: ${errinstanceofError ? err.message : String(err)}`,
235+
};
236+
}
237+
},
195238
onListMemoryRecords: async(memoryName,namespace,strategyId)=>{
196239
constdeployed=awaitresolveDeployedHandlers(baseDir,onLog);
197240
if(!deployed.onListMemoryRecords)return{success: false,error: 'No deployed AgentCore Memory found'};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import{handleGetCloudWatchTrace,handleListCloudWatchTraces}from'../handlers/cloudwatch-traces.js';
2+
importtype{RouteContext}from'../handlers/route-context.js';
3+
importtype{IncomingMessage,ServerResponse}from'http';
4+
import{beforeEach,describe,expect,it,vi}from'vitest';
5+
6+
functionmockRes(): ServerResponse&{_status: number;_headers: Record<string,string>;_body: string}{
7+
constres={
8+
_status: 0,
9+
_headers: {}asRecord<string,string>,
10+
_body: '',
11+
writeHead(status: number,headers?: Record<string,string>){
12+
res._status=status;
13+
if(headers)Object.assign(res._headers,headers);
14+
returnres;
15+
},
16+
setHeader(name: string,value: string){
17+
res._headers[name]=value;
18+
},
19+
end(body?: string){
20+
if(body)res._body=body;
21+
},
22+
};
23+
returnresasunknownasServerResponse&{_status: number;_headers: Record<string,string>;_body: string};
24+
}
25+
26+
functionmockReq(url: string): IncomingMessage{
27+
return{ url,headers: {host: 'localhost:8081'}}asunknownasIncomingMessage;
28+
}
29+
30+
functionmockCtx(overrides: Partial<RouteContext['options']>={}): RouteContext{
31+
return{
32+
options: {
33+
mode: 'dev',
34+
agents: [],
35+
harnesses: [],
36+
uiPort: 8081,
37+
...overrides,
38+
},
39+
runningAgents: newMap(),
40+
startingAgents: newMap(),
41+
agentErrors: newMap(),
42+
setCorsHeaders: vi.fn(),
43+
readBody: vi.fn(),
44+
}asRouteContext;
45+
}
46+
47+
describe('handleListCloudWatchTraces',()=>{
48+
beforeEach(()=>{
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns 404 when no handler configured',async()=>{
53+
constctx=mockCtx();
54+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
55+
constres=mockRes();
56+
57+
awaithandleListCloudWatchTraces(ctx,req,res);
58+
59+
expect(res._status).toBe(404);
60+
constbody=JSON.parse(res._body);
61+
expect(body.success).toBe(false);
62+
expect(body.error).toContain('not available');
63+
});
64+
65+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
66+
consthandler=vi.fn();
67+
constctx=mockCtx({onListCloudWatchTraces: handler});
68+
constreq=mockReq('/api/cloudwatch-traces');
69+
constres=mockRes();
70+
71+
awaithandleListCloudWatchTraces(ctx,req,res);
72+
73+
expect(res._status).toBe(400);
74+
constbody=JSON.parse(res._body);
75+
expect(body.success).toBe(false);
76+
expect(body.error).toContain('agentName');
77+
expect(body.error).toContain('harnessName');
78+
expect(handler).not.toHaveBeenCalled();
79+
});
80+
81+
it('returns 400 when both agentName and harnessName provided',async()=>{
82+
consthandler=vi.fn();
83+
constctx=mockCtx({onListCloudWatchTraces: handler});
84+
constreq=mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h');
85+
constres=mockRes();
86+
87+
awaithandleListCloudWatchTraces(ctx,req,res);
88+
89+
expect(res._status).toBe(400);
90+
constbody=JSON.parse(res._body);
91+
expect(body.success).toBe(false);
92+
expect(body.error).toContain('agentName');
93+
expect(body.error).toContain('harnessName');
94+
expect(handler).not.toHaveBeenCalled();
95+
});
96+
97+
it('calls handler with agentName and returns traces',async()=>{
98+
consttraces=[{traceId: 't1'},{traceId: 't2'}];
99+
consthandler=vi.fn().mockResolvedValue({success: true, traces });
100+
constctx=mockCtx({onListCloudWatchTraces: handler});
101+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
102+
constres=mockRes();
103+
104+
awaithandleListCloudWatchTraces(ctx,req,res);
105+
106+
expect(res._status).toBe(200);
107+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,undefined,undefined);
108+
constbody=JSON.parse(res._body);
109+
expect(body.success).toBe(true);
110+
expect(body.traces).toEqual(traces);
111+
});
112+
113+
it('calls handler with harnessName',async()=>{
114+
consthandler=vi.fn().mockResolvedValue({success: true,traces: []});
115+
constctx=mockCtx({onListCloudWatchTraces: handler});
116+
constreq=mockReq('/api/cloudwatch-traces?harnessName=my-harness');
117+
constres=mockRes();
118+
119+
awaithandleListCloudWatchTraces(ctx,req,res);
120+
121+
expect(res._status).toBe(200);
122+
expect(handler).toHaveBeenCalledWith(undefined,'my-harness',undefined,undefined);
123+
});
124+
125+
it('returns 500 when handler throws',async()=>{
126+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
127+
constctx=mockCtx({onListCloudWatchTraces: handler});
128+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
129+
constres=mockRes();
130+
131+
awaithandleListCloudWatchTraces(ctx,req,res);
132+
133+
expect(res._status).toBe(500);
134+
constbody=JSON.parse(res._body);
135+
expect(body.success).toBe(false);
136+
expect(body.error).toContain('Failed to list CloudWatch traces');
137+
});
138+
139+
it('returns 400 for invalid startTime',async()=>{
140+
consthandler=vi.fn();
141+
constctx=mockCtx({onListCloudWatchTraces: handler});
142+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber');
143+
constres=mockRes();
144+
145+
awaithandleListCloudWatchTraces(ctx,req,res);
146+
147+
expect(res._status).toBe(400);
148+
constbody=JSON.parse(res._body);
149+
expect(body.success).toBe(false);
150+
expect(body.error).toContain('startTime');
151+
expect(handler).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('handleGetCloudWatchTrace',()=>{
156+
beforeEach(()=>{
157+
vi.restoreAllMocks();
158+
});
159+
160+
it('returns 404 when no handler configured',async()=>{
161+
constctx=mockCtx();
162+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
163+
constres=mockRes();
164+
165+
awaithandleGetCloudWatchTrace(ctx,req,res);
166+
167+
expect(res._status).toBe(404);
168+
constbody=JSON.parse(res._body);
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain('not available');
171+
});
172+
173+
it('returns 400 when traceId is missing',async()=>{
174+
consthandler=vi.fn();
175+
constctx=mockCtx({onGetCloudWatchTrace: handler});
176+
constreq=mockReq('/api/cloudwatch-traces/?agentName=my-agent');
177+
constres=mockRes();
178+
179+
awaithandleGetCloudWatchTrace(ctx,req,res);
180+
181+
expect(res._status).toBe(400);
182+
constbody=JSON.parse(res._body);
183+
expect(body.success).toBe(false);
184+
expect(body.error).toContain('traceId');
185+
expect(handler).not.toHaveBeenCalled();
186+
});
187+
188+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
189+
consthandler=vi.fn();
190+
constctx=mockCtx({onGetCloudWatchTrace: handler});
191+
constreq=mockReq('/api/cloudwatch-traces/abc123');
192+
constres=mockRes();
193+
194+
awaithandleGetCloudWatchTrace(ctx,req,res);
195+
196+
expect(res._status).toBe(400);
197+
constbody=JSON.parse(res._body);
198+
expect(body.success).toBe(false);
199+
expect(body.error).toContain('agentName');
200+
expect(body.error).toContain('harnessName');
201+
expect(handler).not.toHaveBeenCalled();
202+
});
203+
204+
it('returns 500 when handler throws',async()=>{
205+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
206+
constctx=mockCtx({onGetCloudWatchTrace: handler});
207+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
208+
constres=mockRes();
209+
210+
awaithandleGetCloudWatchTrace(ctx,req,res);
211+
212+
expect(res._status).toBe(500);
213+
constbody=JSON.parse(res._body);
214+
expect(body.success).toBe(false);
215+
expect(body.error).toContain('Failed to get CloudWatch trace');
216+
});
217+
218+
it('calls handler and returns records',async()=>{
219+
constrecords=[{record: 'data1'}];
220+
consthandler=vi.fn().mockResolvedValue({success: true, records });
221+
constctx=mockCtx({onGetCloudWatchTrace: handler});
222+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
223+
constres=mockRes();
224+
225+
awaithandleGetCloudWatchTrace(ctx,req,res);
226+
227+
expect(res._status).toBe(200);
228+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,'abc123',undefined,undefined);
229+
constbody=JSON.parse(res._body);
230+
expect(body.success).toBe(true);
231+
expect(body.records).toEqual(records);
232+
});
233+
});

‎src/cli/operations/dev/web-ui/api-types.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* TODO: Extract these types into a shared package so both repos import
99
* from a single source of truth instead of manually duplicating.
1010
*/
11+
importtype{CloudWatchSpanRecord,CloudWatchTraceRecord}from'../../traces/types';
1112

1213
// ---------------------------------------------------------------------------
1314
// GET /api/status
@@ -279,6 +280,39 @@ export interface GetTraceResponse {
279280
error?: string;
280281
}
281282

283+
// ---------------------------------------------------------------------------
284+
// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx
285+
// ---------------------------------------------------------------------------
286+
287+
/** A single trace entry returned by the CloudWatch traces list endpoint */
288+
exportinterfaceCloudWatchTraceEntry{
289+
traceId: string;
290+
timestamp: string;
291+
sessionId?: string;
292+
spanCount?: string;
293+
}
294+
295+
/** Response shape for GET /api/cloudwatch-traces */
296+
exportinterfaceListCloudWatchTracesResponse{
297+
success: boolean;
298+
traces?: CloudWatchTraceEntry[];
299+
error?: string;
300+
}
301+
302+
// ---------------------------------------------------------------------------
303+
// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx
304+
// ---------------------------------------------------------------------------
305+
306+
/** Response shape for GET /api/cloudwatch-traces/:traceId */
307+
exportinterfaceGetCloudWatchTraceResponse{
308+
success: boolean;
309+
records?: CloudWatchTraceRecord[];
310+
spans?: CloudWatchSpanRecord[];
311+
error?: string;
312+
}
313+
314+
exporttype{CloudWatchTraceRecord,CloudWatchSpanRecord}from'../../traces/types';
315+
282316
// ---------------------------------------------------------------------------
283317
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
284318
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 76b07aa

Browse files
authored
feat: add CloudWatch traces API for web UI (#997)
1 parent 29b6522 commit 76b07aa

14 files changed

Lines changed: 1244 additions & 197 deletions

File tree

‎src/cli/commands/dev/browser-mode.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
runWebUI,
1010
}from'../../operations/dev/web-ui';
1111
import{listMemoryRecords,retrieveMemoryRecords}from'../../operations/memory';
12+
import{loadDeployedProjectConfig,resolveAgent}from'../../operations/resolve-agent';
13+
import{fetchTraceRecords,listTraces}from'../../operations/traces';
1214
importpathfrom'node:path';
1315

1416
interfaceDeployedHandlers{
@@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
192194
? (agentNameParam,startTime,endTime)=>collector.listTraces(agentNameParam,startTime,endTime)
193195
: undefined,
194196
onGetTrace: collector ? (agentNameParam,traceId)=>collector.getTraceSpans(agentNameParam,traceId) : undefined,
197+
onListCloudWatchTraces: async(agentName,_harnessName,startTime,endTime)=>{
198+
try{
199+
constconfigIO=newConfigIO({ baseDir });
200+
constcontext=awaitloadDeployedProjectConfig(configIO);
201+
constresolved=resolveAgent(context,{runtime: agentName});
202+
if(!resolved.success)return{success: false,error: resolved.error};
203+
returnlistTraces({
204+
region: resolved.agent.region,
205+
runtimeId: resolved.agent.runtimeId,
206+
agentName: resolved.agent.agentName,
207+
startTime,
208+
endTime,
209+
});
210+
}catch(err){
211+
return{
212+
success: false,
213+
error: `Failed to list CloudWatch traces: ${errinstanceofError ? err.message : String(err)}`,
214+
};
215+
}
216+
},
217+
onGetCloudWatchTrace: async(agentName,_harnessName,traceId,startTime,endTime)=>{
218+
try{
219+
constconfigIO=newConfigIO({ baseDir });
220+
constcontext=awaitloadDeployedProjectConfig(configIO);
221+
constresolved=resolveAgent(context,{runtime: agentName});
222+
if(!resolved.success)return{success: false,error: resolved.error};
223+
returnfetchTraceRecords({
224+
region: resolved.agent.region,
225+
runtimeId: resolved.agent.runtimeId,
226+
traceId,
227+
startTime,
228+
endTime,
229+
includeSpans: true,
230+
});
231+
}catch(err){
232+
return{
233+
success: false,
234+
error: `Failed to get CloudWatch trace: ${errinstanceofError ? err.message : String(err)}`,
235+
};
236+
}
237+
},
195238
onListMemoryRecords: async(memoryName,namespace,strategyId)=>{
196239
constdeployed=awaitresolveDeployedHandlers(baseDir,onLog);
197240
if(!deployed.onListMemoryRecords)return{success: false,error: 'No deployed AgentCore Memory found'};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import{handleGetCloudWatchTrace,handleListCloudWatchTraces}from'../handlers/cloudwatch-traces.js';
2+
importtype{RouteContext}from'../handlers/route-context.js';
3+
importtype{IncomingMessage,ServerResponse}from'http';
4+
import{beforeEach,describe,expect,it,vi}from'vitest';
5+
6+
functionmockRes(): ServerResponse&{_status: number;_headers: Record<string,string>;_body: string}{
7+
constres={
8+
_status: 0,
9+
_headers: {}asRecord<string,string>,
10+
_body: '',
11+
writeHead(status: number,headers?: Record<string,string>){
12+
res._status=status;
13+
if(headers)Object.assign(res._headers,headers);
14+
returnres;
15+
},
16+
setHeader(name: string,value: string){
17+
res._headers[name]=value;
18+
},
19+
end(body?: string){
20+
if(body)res._body=body;
21+
},
22+
};
23+
returnresasunknownasServerResponse&{_status: number;_headers: Record<string,string>;_body: string};
24+
}
25+
26+
functionmockReq(url: string): IncomingMessage{
27+
return{ url,headers: {host: 'localhost:8081'}}asunknownasIncomingMessage;
28+
}
29+
30+
functionmockCtx(overrides: Partial<RouteContext['options']>={}): RouteContext{
31+
return{
32+
options: {
33+
mode: 'dev',
34+
agents: [],
35+
harnesses: [],
36+
uiPort: 8081,
37+
...overrides,
38+
},
39+
runningAgents: newMap(),
40+
startingAgents: newMap(),
41+
agentErrors: newMap(),
42+
setCorsHeaders: vi.fn(),
43+
readBody: vi.fn(),
44+
}asRouteContext;
45+
}
46+
47+
describe('handleListCloudWatchTraces',()=>{
48+
beforeEach(()=>{
49+
vi.restoreAllMocks();
50+
});
51+
52+
it('returns 404 when no handler configured',async()=>{
53+
constctx=mockCtx();
54+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
55+
constres=mockRes();
56+
57+
awaithandleListCloudWatchTraces(ctx,req,res);
58+
59+
expect(res._status).toBe(404);
60+
constbody=JSON.parse(res._body);
61+
expect(body.success).toBe(false);
62+
expect(body.error).toContain('not available');
63+
});
64+
65+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
66+
consthandler=vi.fn();
67+
constctx=mockCtx({onListCloudWatchTraces: handler});
68+
constreq=mockReq('/api/cloudwatch-traces');
69+
constres=mockRes();
70+
71+
awaithandleListCloudWatchTraces(ctx,req,res);
72+
73+
expect(res._status).toBe(400);
74+
constbody=JSON.parse(res._body);
75+
expect(body.success).toBe(false);
76+
expect(body.error).toContain('agentName');
77+
expect(body.error).toContain('harnessName');
78+
expect(handler).not.toHaveBeenCalled();
79+
});
80+
81+
it('returns 400 when both agentName and harnessName provided',async()=>{
82+
consthandler=vi.fn();
83+
constctx=mockCtx({onListCloudWatchTraces: handler});
84+
constreq=mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h');
85+
constres=mockRes();
86+
87+
awaithandleListCloudWatchTraces(ctx,req,res);
88+
89+
expect(res._status).toBe(400);
90+
constbody=JSON.parse(res._body);
91+
expect(body.success).toBe(false);
92+
expect(body.error).toContain('agentName');
93+
expect(body.error).toContain('harnessName');
94+
expect(handler).not.toHaveBeenCalled();
95+
});
96+
97+
it('calls handler with agentName and returns traces',async()=>{
98+
consttraces=[{traceId: 't1'},{traceId: 't2'}];
99+
consthandler=vi.fn().mockResolvedValue({success: true, traces });
100+
constctx=mockCtx({onListCloudWatchTraces: handler});
101+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
102+
constres=mockRes();
103+
104+
awaithandleListCloudWatchTraces(ctx,req,res);
105+
106+
expect(res._status).toBe(200);
107+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,undefined,undefined);
108+
constbody=JSON.parse(res._body);
109+
expect(body.success).toBe(true);
110+
expect(body.traces).toEqual(traces);
111+
});
112+
113+
it('calls handler with harnessName',async()=>{
114+
consthandler=vi.fn().mockResolvedValue({success: true,traces: []});
115+
constctx=mockCtx({onListCloudWatchTraces: handler});
116+
constreq=mockReq('/api/cloudwatch-traces?harnessName=my-harness');
117+
constres=mockRes();
118+
119+
awaithandleListCloudWatchTraces(ctx,req,res);
120+
121+
expect(res._status).toBe(200);
122+
expect(handler).toHaveBeenCalledWith(undefined,'my-harness',undefined,undefined);
123+
});
124+
125+
it('returns 500 when handler throws',async()=>{
126+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
127+
constctx=mockCtx({onListCloudWatchTraces: handler});
128+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent');
129+
constres=mockRes();
130+
131+
awaithandleListCloudWatchTraces(ctx,req,res);
132+
133+
expect(res._status).toBe(500);
134+
constbody=JSON.parse(res._body);
135+
expect(body.success).toBe(false);
136+
expect(body.error).toContain('Failed to list CloudWatch traces');
137+
});
138+
139+
it('returns 400 for invalid startTime',async()=>{
140+
consthandler=vi.fn();
141+
constctx=mockCtx({onListCloudWatchTraces: handler});
142+
constreq=mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber');
143+
constres=mockRes();
144+
145+
awaithandleListCloudWatchTraces(ctx,req,res);
146+
147+
expect(res._status).toBe(400);
148+
constbody=JSON.parse(res._body);
149+
expect(body.success).toBe(false);
150+
expect(body.error).toContain('startTime');
151+
expect(handler).not.toHaveBeenCalled();
152+
});
153+
});
154+
155+
describe('handleGetCloudWatchTrace',()=>{
156+
beforeEach(()=>{
157+
vi.restoreAllMocks();
158+
});
159+
160+
it('returns 404 when no handler configured',async()=>{
161+
constctx=mockCtx();
162+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
163+
constres=mockRes();
164+
165+
awaithandleGetCloudWatchTrace(ctx,req,res);
166+
167+
expect(res._status).toBe(404);
168+
constbody=JSON.parse(res._body);
169+
expect(body.success).toBe(false);
170+
expect(body.error).toContain('not available');
171+
});
172+
173+
it('returns 400 when traceId is missing',async()=>{
174+
consthandler=vi.fn();
175+
constctx=mockCtx({onGetCloudWatchTrace: handler});
176+
constreq=mockReq('/api/cloudwatch-traces/?agentName=my-agent');
177+
constres=mockRes();
178+
179+
awaithandleGetCloudWatchTrace(ctx,req,res);
180+
181+
expect(res._status).toBe(400);
182+
constbody=JSON.parse(res._body);
183+
expect(body.success).toBe(false);
184+
expect(body.error).toContain('traceId');
185+
expect(handler).not.toHaveBeenCalled();
186+
});
187+
188+
it('returns 400 when neither agentName nor harnessName provided',async()=>{
189+
consthandler=vi.fn();
190+
constctx=mockCtx({onGetCloudWatchTrace: handler});
191+
constreq=mockReq('/api/cloudwatch-traces/abc123');
192+
constres=mockRes();
193+
194+
awaithandleGetCloudWatchTrace(ctx,req,res);
195+
196+
expect(res._status).toBe(400);
197+
constbody=JSON.parse(res._body);
198+
expect(body.success).toBe(false);
199+
expect(body.error).toContain('agentName');
200+
expect(body.error).toContain('harnessName');
201+
expect(handler).not.toHaveBeenCalled();
202+
});
203+
204+
it('returns 500 when handler throws',async()=>{
205+
consthandler=vi.fn().mockRejectedValue(newError('boom'));
206+
constctx=mockCtx({onGetCloudWatchTrace: handler});
207+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
208+
constres=mockRes();
209+
210+
awaithandleGetCloudWatchTrace(ctx,req,res);
211+
212+
expect(res._status).toBe(500);
213+
constbody=JSON.parse(res._body);
214+
expect(body.success).toBe(false);
215+
expect(body.error).toContain('Failed to get CloudWatch trace');
216+
});
217+
218+
it('calls handler and returns records',async()=>{
219+
constrecords=[{record: 'data1'}];
220+
consthandler=vi.fn().mockResolvedValue({success: true, records });
221+
constctx=mockCtx({onGetCloudWatchTrace: handler});
222+
constreq=mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent');
223+
constres=mockRes();
224+
225+
awaithandleGetCloudWatchTrace(ctx,req,res);
226+
227+
expect(res._status).toBe(200);
228+
expect(handler).toHaveBeenCalledWith('my-agent',undefined,'abc123',undefined,undefined);
229+
constbody=JSON.parse(res._body);
230+
expect(body.success).toBe(true);
231+
expect(body.records).toEqual(records);
232+
});
233+
});

‎src/cli/operations/dev/web-ui/api-types.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* TODO: Extract these types into a shared package so both repos import
99
* from a single source of truth instead of manually duplicating.
1010
*/
11+
importtype{CloudWatchSpanRecord,CloudWatchTraceRecord}from'../../traces/types';
1112

1213
// ---------------------------------------------------------------------------
1314
// GET /api/status
@@ -279,6 +280,39 @@ export interface GetTraceResponse {
279280
error?: string;
280281
}
281282

283+
// ---------------------------------------------------------------------------
284+
// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx
285+
// ---------------------------------------------------------------------------
286+
287+
/** A single trace entry returned by the CloudWatch traces list endpoint */
288+
exportinterfaceCloudWatchTraceEntry{
289+
traceId: string;
290+
timestamp: string;
291+
sessionId?: string;
292+
spanCount?: string;
293+
}
294+
295+
/** Response shape for GET /api/cloudwatch-traces */
296+
exportinterfaceListCloudWatchTracesResponse{
297+
success: boolean;
298+
traces?: CloudWatchTraceEntry[];
299+
error?: string;
300+
}
301+
302+
// ---------------------------------------------------------------------------
303+
// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx
304+
// ---------------------------------------------------------------------------
305+
306+
/** Response shape for GET /api/cloudwatch-traces/:traceId */
307+
exportinterfaceGetCloudWatchTraceResponse{
308+
success: boolean;
309+
records?: CloudWatchTraceRecord[];
310+
spans?: CloudWatchSpanRecord[];
311+
error?: string;
312+
}
313+
314+
exporttype{CloudWatchTraceRecord,CloudWatchSpanRecord}from'../../traces/types';
315+
282316
// ---------------------------------------------------------------------------
283317
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
284318
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)