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
31 changes: 29 additions & 2 deletions packages/shared/src/server/repositories/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2677,6 +2677,7 @@ export const getUsersFromEventsTable = async (
.where(appliedEventsFilter)
.whereRaw("e.user_id IS NOT NULL AND length(e.user_id) > 0")
.whereRaw("e.is_deleted = 0")
.whereRaw("NOT has(e.tags, 'litellm-internal-health-check')")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Align hasAny checks with heartbeat filtering

This exclusion is applied to user list/count/metrics queries, but hasAnyUser/hasAnyUserFromEventsTable still count heartbeat-tagged rows. In projects where heartbeat probes are the only tagged user rows, the users page will report data exists (so onboarding is hidden) while the table endpoints return zero rows, producing an inconsistent empty state. Apply the same heartbeat predicate in the hasAny* queries so the gating logic matches the filtered datasets.

Useful? React with 👍 / 👎.

.when(Boolean(searchQuery), (b) =>
b.whereRaw("e.user_id ILIKE {searchQuery: String}", {
searchQuery: `%${searchQuery}%`,
Expand Down Expand Up @@ -2723,6 +2724,7 @@ export const getUsersCountFromEventsTable = async (
AND e.user_id IS NOT NULL
AND e.user_id != ''
AND e.is_deleted = 0
AND NOT has(e.tags, 'litellm-internal-health-check')
${appliedEventsFilter.query ? `AND ${appliedEventsFilter.query}` : ""}
${searchCondition}
`;
Expand Down Expand Up @@ -2774,13 +2776,27 @@ export const getUserMetricsFromEventsTable = async (
sumMap(e.usage_details) as sum_usage_details,
sum(e.total_cost) as sum_total_cost,
min(e.start_time) as min_timestamp,
max(e.start_time) as max_timestamp
max(e.start_time) as max_timestamp,
anyIf(mapFromArrays(arrayReverse(e.metadata_names), arrayReverse(e.metadata_values))['openclaw_channel'], mapFromArrays(arrayReverse(e.metadata_names), arrayReverse(e.metadata_values))['openclaw_channel'] != '') as openclaw_channel,
anyIf(
coalesce(
nullIf(mapFromArrays(arrayReverse(e.metadata_names), arrayReverse(e.metadata_values))['openclaw_sender_username'], ''),
nullIf(mapFromArrays(arrayReverse(e.metadata_names), arrayReverse(e.metadata_values))['openclaw_sender_name'], ''),
nullIf(mapFromArrays(arrayReverse(e.metadata_names), arrayReverse(e.metadata_values))['openclaw_sender_label'], '')
),
coalesce(
nullIf(mapFromArrays(arrayReverse(e.metadata_names), arrayReverse(e.metadata_values))['openclaw_sender_username'], ''),
nullIf(mapFromArrays(arrayReverse(e.metadata_names), arrayReverse(e.metadata_values))['openclaw_sender_name'], ''),
nullIf(mapFromArrays(arrayReverse(e.metadata_names), arrayReverse(e.metadata_values))['openclaw_sender_label'], '')
) IS NOT NULL
) as openclaw_username
`,
})
.whereRaw("e.user_id IN ({userIds: Array(String)})", { userIds })
// not required if called from tRPC (user_id is always defined), left in for safety only
.whereRaw("e.user_id IS NOT NULL AND length(e.user_id) > 0")
.whereRaw("e.is_deleted = 0")
.whereRaw("NOT has(e.tags, 'litellm-internal-health-check')")
.where(appliedEventsFilter);

const { query: statsQuery, params: statsParams } =
Expand All @@ -2796,6 +2812,8 @@ export const getUserMetricsFromEventsTable = async (
sum_total_cost,
min_timestamp,
max_timestamp,
openclaw_channel,
openclaw_username,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sum_usage_details))) as input_usage,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sum_usage_details))) as output_usage,
sum_usage_details['total'] as total_usage
Expand All @@ -2813,6 +2831,8 @@ export const getUserMetricsFromEventsTable = async (
obs_count: string;
trace_count: string;
sum_total_cost: string;
openclaw_channel: string | null;
openclaw_username: string | null;
}>({
query,
params: statsParams,
Expand All @@ -2835,6 +2855,8 @@ export const getUserMetricsFromEventsTable = 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 All @@ -2845,14 +2867,19 @@ export const getUserMetricsFromEventsTable = async (
export const hasAnyUserFromEventsTable = async (
projectId: string,
): Promise<boolean> => {
// Filter out deleted rows
// Filter out deleted rows + heartbeat probes so the "any user" gate lines
// up with the filtered table/metrics endpoints (see getUsersFromEventsTable,
// getUsersCountFromEventsTable, getUserMetricsFromEventsTable). Otherwise
// projects whose only tagged user rows are LiteLLM health-checks would
// report "data exists" while the list endpoint returns zero.
const query = `
SELECT 1
FROM events_core
WHERE project_id = {projectId: String}
AND user_id IS NOT NULL
AND user_id != ''
AND is_deleted = 0
AND NOT has(tags, 'litellm-internal-health-check')
LIMIT 1
`;

Expand Down
10 changes: 10 additions & 0 deletions packages/shared/src/server/repositories/traces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,7 @@ export const getTracesGroupedByUsers = async (
WHERE t.project_id = {projectId: String}
AND t.user_id IS NOT NULL
AND t.user_id != ''
AND NOT has(t.tags, 'litellm-internal-health-check')
${tracesFilterRes?.query ? `AND ${tracesFilterRes.query}` : ""}
${search.query}
GROUP BY user
Expand Down Expand Up @@ -1120,12 +1121,18 @@ export const hasAnyUser = async (projectId: string) => {
},
},
fn: async (input) => {
// The heartbeat-tag filter mirrors the one on getUsersCountFromEventsTable
// and getTracesGroupedByUsers — without it, projects whose only tagged
// user rows are LiteLLM health-check probes would report "data exists"
// here while the filtered table/metrics endpoints return zero rows,
// leaving the onboarding wizard permanently hidden.
const query = `
SELECT 1
FROM traces
WHERE project_id = {projectId: String}
AND user_id IS NOT NULL
AND user_id != ''
AND NOT has(tags, 'litellm-internal-health-check')
LIMIT 1
`;

Expand Down Expand Up @@ -1186,6 +1193,7 @@ export const getTotalUserCount = async (
${search.query}
AND t.user_id IS NOT NULL
AND t.user_id != ''
AND NOT has(t.tags, 'litellm-internal-health-check')
`;

return queryClickhouse({
Expand Down Expand Up @@ -1259,6 +1267,7 @@ export const getUserMetrics = async (
where
user_id IN ({userIds: Array(String) })
AND project_id = {projectId: String }
AND NOT has(t.tags, 'litellm-internal-health-check')
${filter.length > 0 ? `AND ${chFilterRes.query}` : ""}
)
) as o
Expand All @@ -1280,6 +1289,7 @@ export const getUserMetrics = async (
WHERE
t.user_id IN ({userIds: Array(String) })
AND t.project_id = {projectId: String }
AND NOT has(t.tags, 'litellm-internal-health-check')
${filter.length > 0 ? `AND ${chFilterRes.query}` : ""}
) as t on t.id = o.trace_id
and t.project_id = o.project_id
Expand Down
22 changes: 22 additions & 0 deletions web/src/components/table/use-cases/traces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,28 @@ export default function TracesTable({
return value ?? undefined;
},
},
{
accessorKey: "model",
header: "Model",
id: "model",
size: 150,
enableHiding: true,
enableSorting: false,
cell: ({ row }) => {
// `TracesTableUiReturnType` does not include metadata, so we can
// only derive the model from fields already in the row. LiteLLM
// sets trace name to `litellm-<endpoint>/<model>` — parse the
// segment after the slash.
const name: TracesTableRow["name"] = row.getValue("name");
if (typeof name === "string" && name.startsWith("litellm-")) {
const slashIdx = name.indexOf("/");
if (slashIdx >= 0 && slashIdx < name.length - 1) {
return name.slice(slashIdx + 1);
}
}
return undefined;
},
},
{
accessorKey: "input",
header: "Input",
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 @@ -194,6 +194,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