diff --git a/packages/shared/src/server/repositories/events.ts b/packages/shared/src/server/repositories/events.ts index 41c858b6a0e4..c2ac2254a690 100644 --- a/packages/shared/src/server/repositories/events.ts +++ b/packages/shared/src/server/repositories/events.ts @@ -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')") .when(Boolean(searchQuery), (b) => b.whereRaw("e.user_id ILIKE {searchQuery: String}", { searchQuery: `%${searchQuery}%`, @@ -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} `; @@ -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 } = @@ -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 @@ -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, @@ -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, })); }; @@ -2845,7 +2867,11 @@ export const getUserMetricsFromEventsTable = async ( export const hasAnyUserFromEventsTable = async ( projectId: string, ): Promise => { - // 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 @@ -2853,6 +2879,7 @@ export const hasAnyUserFromEventsTable = async ( AND user_id IS NOT NULL AND user_id != '' AND is_deleted = 0 + AND NOT has(tags, 'litellm-internal-health-check') LIMIT 1 `; diff --git a/packages/shared/src/server/repositories/traces.ts b/packages/shared/src/server/repositories/traces.ts index 8fb768043749..8a8c169c848c 100644 --- a/packages/shared/src/server/repositories/traces.ts +++ b/packages/shared/src/server/repositories/traces.ts @@ -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 @@ -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 `; @@ -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({ @@ -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 @@ -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 diff --git a/web/src/components/table/use-cases/traces.tsx b/web/src/components/table/use-cases/traces.tsx index 659098861e9b..066db8552421 100644 --- a/web/src/components/table/use-cases/traces.tsx +++ b/web/src/components/table/use-cases/traces.tsx @@ -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-/` — 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", diff --git a/web/src/server/api/routers/users.ts b/web/src/server/api/routers/users.ts index ad6d5493180a..30d1945498f1 100644 --- a/web/src/server/api/routers/users.ts +++ b/web/src/server/api/routers/users.ts @@ -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, })); }),