Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions packages/shared/src/server/repositories/traces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,7 +1232,9 @@ export const getUserMetrics = async (
sum(total_cost) as sum_total_cost,
max(t.timestamp) as max_timestamp,
min(t.timestamp) as min_timestamp,
count(distinct t.id) as trace_count
count(distinct t.id) as trace_count,
anyIf(t.openclaw_channel, t.openclaw_channel != '') as openclaw_channel,
anyIf(t.openclaw_username, t.openclaw_username != '') as openclaw_username
FROM
(
SELECT
Expand Down Expand Up @@ -1266,7 +1268,13 @@ export const getUserMetrics = async (
t.user_id,
t.project_id,
t.timestamp,
t.environment
t.environment,
t.metadata['openclaw_channel'] as openclaw_channel,
coalesce(
nullIf(t.metadata['openclaw_sender_username'], ''),
nullIf(t.metadata['openclaw_sender_name'], ''),
nullIf(t.metadata['openclaw_sender_label'], '')
) as openclaw_username
FROM
__TRACE_TABLE__ t FINAL
WHERE
Expand All @@ -1288,7 +1296,9 @@ export const getUserMetrics = async (
environment,
sum_total_cost,
max_timestamp,
min_timestamp
min_timestamp,
openclaw_channel,
openclaw_username
FROM stats`;

return measureAndReturn({
Expand Down Expand Up @@ -1327,6 +1337,8 @@ export const getUserMetrics = async (
obs_count: string;
trace_count: string;
sum_total_cost: string;
openclaw_channel: string | null;
openclaw_username: string | null;
}>({
query: query.replaceAll("__TRACE_TABLE__", "traces"),
params: input.params,
Expand All @@ -1344,6 +1356,8 @@ export const getUserMetrics = async (
observationCount: Number(row.obs_count),
traceCount: Number(row.trace_count),
totalCost: Number(row.sum_total_cost),
openclawChannel: row.openclaw_channel || null,
openclawUsername: row.openclaw_username || null,
}));
},
});
Expand Down
87 changes: 87 additions & 0 deletions web/src/__tests__/server/users-ui-table.servertest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,91 @@ describe("getUserMetrics function", () => {
totalCost: 125, // 50 + 75
});
});

it("surfaces openclaw_channel and coalesced openclaw sender identity", async () => {
const userId = uuidv4();
const traceA = uuidv4();
const traceB = uuidv4();

await createTracesCh([
createTrace({
id: traceA,
project_id: projectId,
user_id: userId,
metadata: {
openclaw_channel: "mattermost",
// username absent on this trace - falls back to name.
openclaw_sender_name: "@ziabinartem",
openclaw_sender_label: "@ziabinartem (pwanex3ne3fx58nr5oaxg4j54w)",
},
}),
createTrace({
id: traceB,
project_id: projectId,
user_id: userId,
metadata: {
openclaw_channel: "mattermost",
openclaw_sender_username: "ziabinartem",
},
}),
]);

// One observation per trace is required for the inner join to return rows.
await createObservationsInClickhouse([
createObservationObject({
id: uuidv4(),
trace_id: traceA,
project_id: projectId,
type: "GENERATION",
}),
createObservationObject({
id: uuidv4(),
trace_id: traceB,
project_id: projectId,
type: "GENERATION",
}),
]);

const result = await getUserMetrics(projectId, [userId], []);
expect(result).toHaveLength(1);
expect(result[0].openclawChannel).toBe("mattermost");
// username resolves via coalesce priority - one trace has username, the
// other falls back to name. Either non-empty value is acceptable since
// anyIf does not guarantee ordering.
expect(
["ziabinartem", "@ziabinartem"].includes(
result[0].openclawUsername ?? "",
),
).toBe(true);
});

it("returns null openclaw fields when trace metadata is missing", async () => {
const userId = uuidv4();
const traceId = uuidv4();

await createTracesCh([
createTrace({
id: traceId,
project_id: projectId,
user_id: userId,
metadata: {
source: "API",
},
}),
]);

await createObservationsInClickhouse([
createObservationObject({
id: uuidv4(),
trace_id: traceId,
project_id: projectId,
type: "GENERATION",
}),
]);

const result = await getUserMetrics(projectId, [userId], []);
expect(result).toHaveLength(1);
expect(result[0].openclawChannel).toBeNull();
expect(result[0].openclawUsername).toBeNull();
});
});
47 changes: 47 additions & 0 deletions web/src/pages/project/[projectId]/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { Badge } from "@/src/components/ui/badge";

type RowData = {
userId: string;
channel?: string;
username?: string;
environment?: string;
firstEvent: string;
lastEvent: string;
Expand Down Expand Up @@ -303,6 +305,49 @@ const UsersTable = ({ isBetaEnabled }: { isBetaEnabled: boolean }) => {
) : undefined;
},
},
{
accessorKey: "channel",
header: "Channel",
id: "channel",
size: 120,
enableHiding: true,
headerTooltip: {
description:
"OpenClaw source channel (e.g. telegram, mattermost) derived from trace metadata.",
},
cell: ({ row }) => {
const value: RowData["channel"] = row.getValue("channel");
if (!userMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ? (
<Badge
variant="secondary"
className="max-w-fit truncate rounded-sm px-1 font-normal"
>
{value}
</Badge>
) : null;
},
},
{
accessorKey: "username",
header: "Username",
id: "username",
size: 180,
enableHiding: true,
headerTooltip: {
description:
"Human-readable sender name from OpenClaw trace metadata (username / name / label in that order).",
},
cell: ({ row }) => {
const value: RowData["username"] = row.getValue("username");
if (!userMetrics.isSuccess) {
return <Skeleton className="h-3 w-1/2" />;
}
return value ?? null;
},
},
{
accessorKey: "environment",
header: "Environment",
Expand Down Expand Up @@ -444,6 +489,8 @@ const UsersTable = ({ isBetaEnabled }: { isBetaEnabled: boolean }) => {
data: userRowData.rows?.map((t) => {
return {
userId: t.id,
channel: t.openclawChannel ?? undefined,
username: t.openclawUsername ?? undefined,
environment: t.environment ?? undefined,
firstEvent:
t.firstTrace?.toLocaleString() ?? "No event yet",
Expand Down
2 changes: 2 additions & 0 deletions web/src/server/api/routers/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export const userRouter = createTRPCRouter({
totalObservations: BigInt(metric.observationCount),
totalTraces: BigInt(metric.traceCount),
sumCalculatedTotalCost: metric.totalCost,
openclawChannel: metric.openclawChannel,
openclawUsername: metric.openclawUsername,
}));
}),

Expand Down
Loading