Skip to content

Commit 9306faf

Browse files
feat(admin): add All Events tab to KiloClaw instance detail (#1703)
Adds a paginated All Events tab alongside the existing DO & Reconcile tab in the instance events card, querying all delivery types (http, do, reconcile, queue) across all identifiers for the instance. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
1 parent 4418605 commit 9306faf

File tree

3 files changed

+343
-95
lines changed

3 files changed

+343
-95
lines changed

src/app/admin/api/kiloclaw-analytics/hooks.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export type KiloclawEventRow = {
1919
value: number;
2020
};
2121

22+
export type KiloclawAllEventRow = KiloclawEventRow & {
23+
user_id: string;
24+
sandbox_id: string;
25+
};
26+
2227
type AnalyticsEngineResponse<T> = {
2328
data: T[];
2429
meta: { name: string; type: string }[];
@@ -41,3 +46,42 @@ export function useKiloclawInstanceEvents(sandboxId: string) {
4146
refetchInterval: 60000,
4247
});
4348
}
49+
50+
type AllEventsParams = {
51+
sandboxId: string;
52+
userId: string;
53+
flyAppName?: string | null;
54+
flyMachineId?: string | null;
55+
offset: number;
56+
};
57+
58+
export function useKiloclawAllEvents(params: AllEventsParams) {
59+
const { sandboxId, userId, flyAppName, flyMachineId, offset } = params;
60+
return useQuery<AnalyticsEngineResponse<KiloclawAllEventRow>>({
61+
queryKey: [
62+
'kiloclaw-analytics',
63+
'all-events',
64+
sandboxId,
65+
userId,
66+
flyAppName,
67+
flyMachineId,
68+
offset,
69+
],
70+
queryFn: async () => {
71+
const searchParams = new URLSearchParams({
72+
query: 'all-events',
73+
sandboxId,
74+
userId,
75+
offset: String(offset),
76+
});
77+
if (flyAppName) searchParams.set('flyAppName', flyAppName);
78+
if (flyMachineId) searchParams.set('flyMachineId', flyMachineId);
79+
const response = await fetch(`/admin/api/kiloclaw-analytics?${searchParams.toString()}`);
80+
if (!response.ok) {
81+
throw new Error('Failed to fetch kiloclaw all events');
82+
}
83+
return response.json() as Promise<AnalyticsEngineResponse<KiloclawAllEventRow>>;
84+
},
85+
enabled: !!sandboxId && !!userId,
86+
});
87+
}

src/app/admin/api/kiloclaw-analytics/route.ts

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@ import { NextResponse } from 'next/server';
33
import { getUserFromAuth } from '@/lib/user.server';
44
import { getEnvVariable } from '@/lib/dotenvx';
55

6-
type QueryType = 'instance-events';
6+
type QueryType = 'instance-events' | 'all-events';
77

8-
const validQueryTypes = new Set<QueryType>(['instance-events']);
8+
const validQueryTypes = new Set<QueryType>(['instance-events', 'all-events']);
99

10-
function buildQuery(queryType: QueryType, sandboxId: string): string {
11-
// sandboxId is validated as base64url [A-Za-z0-9_-]+ before reaching here
10+
// Validates that a value is safe to interpolate into SQL (alphanumeric, hyphens, underscores only)
11+
function isSafeIdentifier(value: string): boolean {
12+
return /^[A-Za-z0-9_-]+$/.test(value);
13+
}
14+
15+
type AllEventsParams = {
16+
sandboxId: string;
17+
userId: string;
18+
flyAppName: string | null;
19+
flyMachineId: string | null;
20+
offset: number;
21+
};
22+
23+
function buildQuery(queryType: QueryType, sandboxId: string, params?: AllEventsParams): string {
1224
switch (queryType) {
1325
case 'instance-events':
26+
// sandboxId is validated as base64url [A-Za-z0-9_-]+ before reaching here
1427
return `SELECT
1528
timestamp,
1629
blob1 AS event,
@@ -33,6 +46,40 @@ WHERE
3346
ORDER BY timestamp DESC
3447
LIMIT 20
3548
FORMAT JSON`;
49+
50+
case 'all-events': {
51+
// All identifiers are validated before reaching here
52+
const p = params as AllEventsParams;
53+
const orClauses = [`blob2 = '${p.userId}'`, `blob8 = '${p.sandboxId}'`];
54+
if (p.flyMachineId) orClauses.push(`blob7 = '${p.flyMachineId}'`);
55+
if (p.flyAppName) orClauses.push(`blob6 = '${p.flyAppName}'`);
56+
return `SELECT
57+
timestamp,
58+
blob1 AS event,
59+
blob2 AS user_id,
60+
blob3 AS delivery,
61+
blob4 AS route,
62+
blob5 AS error,
63+
blob6 AS fly_app_name,
64+
blob7 AS fly_machine_id,
65+
blob8 AS sandbox_id,
66+
blob9 AS status,
67+
blob10 AS openclaw_version,
68+
blob11 AS image_tag,
69+
blob12 AS fly_region,
70+
blob13 AS label,
71+
double1 AS duration_ms,
72+
double2 AS value
73+
FROM kiloclaw_events
74+
WHERE
75+
(${orClauses.join(' OR ')})
76+
AND blob3 IN ('http', 'do', 'reconcile', 'queue')
77+
AND blob1 != 'platform.gateway.status.get'
78+
ORDER BY timestamp DESC
79+
LIMIT 100
80+
OFFSET ${p.offset}
81+
FORMAT JSON`;
82+
}
3683
}
3784
}
3885

@@ -61,7 +108,7 @@ export async function GET(
61108
);
62109
}
63110

64-
if (!sandboxId || !/^[A-Za-z0-9_-]+$/.test(sandboxId)) {
111+
if (!sandboxId || !isSafeIdentifier(sandboxId)) {
65112
return NextResponse.json({ error: 'Invalid or missing sandboxId' }, { status: 400 });
66113
}
67114

@@ -75,7 +122,38 @@ export async function GET(
75122
);
76123
}
77124

78-
const sqlQuery = buildQuery(queryType as QueryType, sandboxId);
125+
let sqlQuery: string;
126+
127+
if (queryType === 'all-events') {
128+
const userId = searchParams.get('userId');
129+
const flyAppName = searchParams.get('flyAppName');
130+
const flyMachineId = searchParams.get('flyMachineId');
131+
const offsetParam = searchParams.get('offset');
132+
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
133+
134+
if (!userId || !isSafeIdentifier(userId)) {
135+
return NextResponse.json({ error: 'Invalid or missing userId' }, { status: 400 });
136+
}
137+
if (flyAppName && !isSafeIdentifier(flyAppName)) {
138+
return NextResponse.json({ error: 'Invalid flyAppName' }, { status: 400 });
139+
}
140+
if (flyMachineId && !isSafeIdentifier(flyMachineId)) {
141+
return NextResponse.json({ error: 'Invalid flyMachineId' }, { status: 400 });
142+
}
143+
if (isNaN(offset) || offset < 0) {
144+
return NextResponse.json({ error: 'Invalid offset' }, { status: 400 });
145+
}
146+
147+
sqlQuery = buildQuery('all-events', sandboxId, {
148+
sandboxId,
149+
userId,
150+
flyAppName: flyAppName ?? null,
151+
flyMachineId: flyMachineId ?? null,
152+
offset,
153+
});
154+
} else {
155+
sqlQuery = buildQuery(queryType as QueryType, sandboxId);
156+
}
79157

80158
const response = await fetch(
81159
`https://api.cloudflare.com/client/v4/accounts/${accountId}/analytics_engine/sql`,

0 commit comments

Comments
 (0)