77 workflowExecutionLogs ,
88 workspace ,
99} from '@sim/db/schema'
10- import { and , eq , sql } from 'drizzle-orm'
10+ import { and , eq , type SQL , sql } from 'drizzle-orm'
1111import {
1212 ACTIVITY_PAGE_SIZE ,
1313 type ActivityAggregate ,
@@ -32,11 +32,16 @@ export async function readActivityWorkspace(organizationId: string, workspaceId:
3232 * Only lightweight execution columns are read; transcripts and trace payloads stay private.
3333 * Chat continuations share an execution id and belong to their first retained start.
3434 */
35- function activityCte ( scope : ActivityScope , dimension ?: ActivityDimension ) {
35+ function activityGroups (
36+ scope : ActivityScope ,
37+ keys : SQL ,
38+ grouping : SQL ,
39+ dimension ?: ActivityDimension
40+ ) {
3641 const start = sql `(${ scope . start . toISOString ( ) } ::timestamptz AT TIME ZONE 'UTC')`
3742 const end = sql `(${ scope . end . toISOString ( ) } ::timestamptz AT TIME ZONE 'UTC')`
3843 const workflows = sql `
39- SELECT 'workflow' AS kind, l.workspace_id, l.workflow_id, NULL::text AS member_id ,
44+ SELECT l.workspace_id, l.workflow_id,
4045 l.trigger, l.started_at, l.status,
4146 CASE WHEN l.status IN ('completed', 'failed') AND l.total_duration_ms >= 0
4247 THEN l.total_duration_ms END AS duration_ms
@@ -60,8 +65,7 @@ function activityCte(scope: ActivityScope, dimension?: ActivityDimension) {
6065 WHERE c.organization_id = ${ scope . organizationId } `
6166 const chats = sql `
6267 SELECT DISTINCT ON (r.execution_id)
63- 'chat' AS kind, c.workspace_id, NULL::text AS workflow_id, r.user_id AS member_id,
64- NULL::text AS trigger, r.started_at, NULL::text AS status, NULL::integer AS duration_ms
68+ c.workspace_id, r.user_id AS member_id, r.started_at
6569 FROM ${ copilotRuns } r
6670 JOIN (${ scopedChats } ) c ON c.id = r.chat_id
6771 WHERE r.started_at >= ${ start } AND r.started_at < ${ end }
@@ -71,34 +75,45 @@ function activityCte(scope: ActivityScope, dimension?: ActivityDimension) {
7175 )
7276 ORDER BY r.execution_id, r.started_at, r.id
7377 `
74- const source =
75- dimension === 'member'
76- ? chats
77- : dimension === 'workflow' || dimension === 'trigger'
78- ? workflows
79- : sql `(${ workflows } ) UNION ALL (${ chats } )`
80- return sql `WITH activity AS (${ source } )`
78+ const workflowGroups = sql `
79+ SELECT ${ keys } , count(*) AS "workflowRuns",
80+ count(*) FILTER (WHERE a.status = 'completed') AS completed,
81+ count(*) FILTER (WHERE a.status = 'failed') AS failed,
82+ 0::bigint AS "chatRuns", 0::bigint AS "chatMembers",
83+ avg(a.duration_ms) AS "averageDurationMs"
84+ FROM (${ workflows } ) a ${ grouping }
85+ `
86+ const chatGroups = sql `
87+ SELECT ${ keys } , 0::bigint AS "workflowRuns", 0::bigint AS completed, 0::bigint AS failed,
88+ count(*) AS "chatRuns", count(DISTINCT a.member_id) AS "chatMembers",
89+ NULL::numeric AS "averageDurationMs"
90+ FROM (${ chats } ) a ${ grouping }
91+ `
92+ if ( dimension === 'member' ) return chatGroups
93+ if ( dimension === 'workflow' || dimension === 'trigger' ) return workflowGroups
94+ return sql `(${ workflowGroups } ) UNION ALL (${ chatGroups } )`
8195}
8296
97+ /** Each group has at most one workflow average and one exact chat-member count. */
8398const aggregates = sql `
84- count(*) FILTER (WHERE a.kind = 'workflow' ) AS "workflowRuns",
85- count(*) FILTER (WHERE a.kind = 'workflow' AND a.status = ' completed' ) AS completed,
86- count(*) FILTER (WHERE a.kind = 'workflow' AND a.status = ' failed' ) AS failed,
87- count(*) FILTER (WHERE a.kind = 'chat' ) AS "chatRuns",
88- count(DISTINCT a.member_id ) AS "chatMembers",
89- avg (a.duration_ms ) AS "averageDurationMs"
99+ sum(a."workflowRuns" ) AS "workflowRuns",
100+ sum(a. completed) AS completed,
101+ sum(a. failed) AS failed,
102+ sum(a."chatRuns" ) AS "chatRuns",
103+ sum(a."chatMembers" ) AS "chatMembers",
104+ max (a."averageDurationMs" ) AS "averageDurationMs"
90105`
91106
92107export async function readActivitySummary (
93108 scope : ActivityScope ,
94109 bucket : UsageBucket ,
95110 timezone : string
96111) {
112+ const keys = sql `date_trunc(${ bucket } , (a.started_at AT TIME ZONE 'UTC') AT TIME ZONE ${ timezone } ) AS bucket`
97113 const rows = await dbReplica . execute < ActivityAggregate & { bucket : string | null } > ( sql `
98- ${ activityCte ( scope ) }
99- SELECT to_char(date_trunc(${ bucket } , (started_at AT TIME ZONE 'UTC') AT TIME ZONE ${ timezone } ),
100- 'YYYY-MM-DD') AS bucket, ${ aggregates }
101- FROM activity a GROUP BY GROUPING SETS ((1), ())
114+ WITH activity AS (${ activityGroups ( scope , keys , sql `GROUP BY GROUPING SETS ((1), ())` ) } )
115+ SELECT to_char(a.bucket, 'YYYY-MM-DD') AS bucket, ${ aggregates }
116+ FROM activity a GROUP BY a.bucket
102117 ` )
103118 return {
104119 totals : activityMetrics ( rows . find ( ( row ) => row . bucket === null ) ) ,
@@ -152,8 +167,13 @@ export async function readActivityBreakdown(
152167 workspaceName : string | null
153168 }
154169 > ( sql `
155- ${ activityCte ( scope , dimension ) } , grouped AS (
156- SELECT ${ id } AS id, ${ workspaceId } AS "workspaceId", ${ aggregates }
170+ WITH activity AS (${ activityGroups (
171+ scope ,
172+ sql `${ id } AS id, ${ workspaceId } AS "workspaceId"` ,
173+ sql `GROUP BY 1, 2` ,
174+ dimension
175+ ) } ), grouped AS (
176+ SELECT a.id, a."workspaceId", ${ aggregates }
157177 FROM activity a
158178 GROUP BY 1, 2
159179 ), named AS (
0 commit comments