Skip to content

Commit a40243f

Browse files
committed
Merge branch 'staging' into feat/workflow-as-mcp
2 parents 8b4f2a0 + 1ddbac1 commit a40243f

34 files changed

Lines changed: 17668 additions & 119 deletions

File tree

apps/sim/app/api/billing/update-cost/route.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { userStats } from '@sim/db/schema'
33
import { eq, sql } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { z } from 'zod'
6+
import { logModelUsage } from '@/lib/billing/core/usage-log'
67
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
78
import { checkInternalApiKey } from '@/lib/copilot/utils'
89
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
@@ -14,6 +15,9 @@ const logger = createLogger('BillingUpdateCostAPI')
1415
const UpdateCostSchema = z.object({
1516
userId: z.string().min(1, 'User ID is required'),
1617
cost: z.number().min(0, 'Cost must be a non-negative number'),
18+
model: z.string().min(1, 'Model is required'),
19+
inputTokens: z.number().min(0).default(0),
20+
outputTokens: z.number().min(0).default(0),
1721
})
1822

1923
/**
@@ -71,11 +75,12 @@ export async function POST(req: NextRequest) {
7175
)
7276
}
7377

74-
const { userId, cost } = validation.data
78+
const { userId, cost, model, inputTokens, outputTokens } = validation.data
7579

7680
logger.info(`[${requestId}] Processing cost update`, {
7781
userId,
7882
cost,
83+
model,
7984
})
8085

8186
// Check if user stats record exists (same as ExecutionLogger)
@@ -107,6 +112,16 @@ export async function POST(req: NextRequest) {
107112
addedCost: cost,
108113
})
109114

115+
// Log usage for complete audit trail
116+
await logModelUsage({
117+
userId,
118+
source: 'copilot',
119+
model,
120+
inputTokens,
121+
outputTokens,
122+
cost,
123+
})
124+
110125
// Check if user has hit overage threshold and bill incrementally
111126
await checkAndBillOverageThreshold(userId)
112127

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { randomUUID } from 'crypto'
22
import { db } from '@sim/db'
3-
import { chat } from '@sim/db/schema'
3+
import { chat, workflow } from '@sim/db/schema'
44
import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
@@ -94,6 +94,21 @@ export async function POST(
9494
if (!deployment.isActive) {
9595
logger.warn(`[${requestId}] Chat is not active: ${identifier}`)
9696

97+
const [workflowRecord] = await db
98+
.select({ workspaceId: workflow.workspaceId })
99+
.from(workflow)
100+
.where(eq(workflow.id, deployment.workflowId))
101+
.limit(1)
102+
103+
const workspaceId = workflowRecord?.workspaceId
104+
if (!workspaceId) {
105+
logger.warn(`[${requestId}] Cannot log: workflow ${deployment.workflowId} has no workspace`)
106+
return addCorsHeaders(
107+
createErrorResponse('This chat is currently unavailable', 403),
108+
request
109+
)
110+
}
111+
97112
const executionId = randomUUID()
98113
const loggingSession = new LoggingSession(
99114
deployment.workflowId,
@@ -104,7 +119,7 @@ export async function POST(
104119

105120
await loggingSession.safeStart({
106121
userId: deployment.userId,
107-
workspaceId: '', // Will be resolved if needed
122+
workspaceId,
108123
variables: {},
109124
})
110125

@@ -169,7 +184,14 @@ export async function POST(
169184

170185
const { actorUserId, workflowRecord } = preprocessResult
171186
const workspaceOwnerId = actorUserId!
172-
const workspaceId = workflowRecord?.workspaceId || ''
187+
const workspaceId = workflowRecord?.workspaceId
188+
if (!workspaceId) {
189+
logger.error(`[${requestId}] Workflow ${deployment.workflowId} has no workspaceId`)
190+
return addCorsHeaders(
191+
createErrorResponse('Workflow has no associated workspace', 500),
192+
request
193+
)
194+
}
173195

174196
try {
175197
const selectedOutputs: string[] = []

apps/sim/app/api/logs/export/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export async function GET(request: NextRequest) {
5757
workflowName: workflow.name,
5858
}
5959

60-
let conditions: SQL | undefined = eq(workflow.workspaceId, params.workspaceId)
60+
let conditions: SQL | undefined = eq(workflowExecutionLogs.workspaceId, params.workspaceId)
6161

6262
if (params.level && params.level !== 'all') {
6363
const levels = params.level.split(',').filter(Boolean)
@@ -134,7 +134,7 @@ export async function GET(request: NextRequest) {
134134
permissions,
135135
and(
136136
eq(permissions.entityType, 'workspace'),
137-
eq(permissions.entityId, workflow.workspaceId),
137+
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
138138
eq(permissions.userId, userId)
139139
)
140140
)

apps/sim/app/api/logs/route.ts

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export async function GET(request: NextRequest) {
130130
deploymentVersionName: sql<null>`NULL`,
131131
}
132132

133+
const workspaceFilter = eq(workflowExecutionLogs.workspaceId, params.workspaceId)
134+
133135
const baseQuery = db
134136
.select(selectColumns)
135137
.from(workflowExecutionLogs)
@@ -141,18 +143,12 @@ export async function GET(request: NextRequest) {
141143
workflowDeploymentVersion,
142144
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
143145
)
144-
.innerJoin(
145-
workflow,
146-
and(
147-
eq(workflowExecutionLogs.workflowId, workflow.id),
148-
eq(workflow.workspaceId, params.workspaceId)
149-
)
150-
)
146+
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
151147
.innerJoin(
152148
permissions,
153149
and(
154150
eq(permissions.entityType, 'workspace'),
155-
eq(permissions.entityId, workflow.workspaceId),
151+
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
156152
eq(permissions.userId, userId)
157153
)
158154
)
@@ -300,7 +296,7 @@ export async function GET(request: NextRequest) {
300296
}
301297

302298
const logs = await baseQuery
303-
.where(conditions)
299+
.where(and(workspaceFilter, conditions))
304300
.orderBy(desc(workflowExecutionLogs.startedAt))
305301
.limit(params.limit)
306302
.offset(params.offset)
@@ -312,22 +308,16 @@ export async function GET(request: NextRequest) {
312308
pausedExecutions,
313309
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
314310
)
315-
.innerJoin(
316-
workflow,
317-
and(
318-
eq(workflowExecutionLogs.workflowId, workflow.id),
319-
eq(workflow.workspaceId, params.workspaceId)
320-
)
321-
)
311+
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
322312
.innerJoin(
323313
permissions,
324314
and(
325315
eq(permissions.entityType, 'workspace'),
326-
eq(permissions.entityId, workflow.workspaceId),
316+
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
327317
eq(permissions.userId, userId)
328318
)
329319
)
330-
.where(conditions)
320+
.where(and(eq(workflowExecutionLogs.workspaceId, params.workspaceId), conditions))
331321

332322
const countResult = await countQuery
333323

apps/sim/app/api/logs/triggers/route.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@sim/db'
2-
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
2+
import { permissions, workflowExecutionLogs } from '@sim/db/schema'
33
import { and, eq, isNotNull, sql } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { z } from 'zod'
@@ -42,23 +42,17 @@ export async function GET(request: NextRequest) {
4242
trigger: workflowExecutionLogs.trigger,
4343
})
4444
.from(workflowExecutionLogs)
45-
.innerJoin(
46-
workflow,
47-
and(
48-
eq(workflowExecutionLogs.workflowId, workflow.id),
49-
eq(workflow.workspaceId, params.workspaceId)
50-
)
51-
)
5245
.innerJoin(
5346
permissions,
5447
and(
5548
eq(permissions.entityType, 'workspace'),
56-
eq(permissions.entityId, workflow.workspaceId),
49+
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
5750
eq(permissions.userId, userId)
5851
)
5952
)
6053
.where(
6154
and(
55+
eq(workflowExecutionLogs.workspaceId, params.workspaceId),
6256
isNotNull(workflowExecutionLogs.trigger),
6357
sql`${workflowExecutionLogs.trigger} NOT IN ('api', 'manual', 'webhook', 'chat', 'schedule')`
6458
)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { type NextRequest, NextResponse } from 'next/server'
2+
import { z } from 'zod'
3+
import { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log'
5+
import { createLogger } from '@/lib/logs/console/logger'
6+
7+
const logger = createLogger('UsageLogsAPI')
8+
9+
const QuerySchema = z.object({
10+
source: z.enum(['workflow', 'wand', 'copilot']).optional(),
11+
workspaceId: z.string().optional(),
12+
period: z.enum(['1d', '7d', '30d', 'all']).optional().default('30d'),
13+
limit: z.coerce.number().min(1).max(100).optional().default(50),
14+
cursor: z.string().optional(),
15+
})
16+
17+
/**
18+
* GET /api/users/me/usage-logs
19+
* Get usage logs for the authenticated user
20+
*/
21+
export async function GET(req: NextRequest) {
22+
try {
23+
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
24+
25+
if (!auth.success || !auth.userId) {
26+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
27+
}
28+
29+
const userId = auth.userId
30+
31+
const { searchParams } = new URL(req.url)
32+
const queryParams = {
33+
source: searchParams.get('source') || undefined,
34+
workspaceId: searchParams.get('workspaceId') || undefined,
35+
period: searchParams.get('period') || '30d',
36+
limit: searchParams.get('limit') || '50',
37+
cursor: searchParams.get('cursor') || undefined,
38+
}
39+
40+
const validation = QuerySchema.safeParse(queryParams)
41+
42+
if (!validation.success) {
43+
return NextResponse.json(
44+
{
45+
error: 'Invalid query parameters',
46+
details: validation.error.issues,
47+
},
48+
{ status: 400 }
49+
)
50+
}
51+
52+
const { source, workspaceId, period, limit, cursor } = validation.data
53+
54+
let startDate: Date | undefined
55+
const endDate = new Date()
56+
57+
if (period !== 'all') {
58+
startDate = new Date()
59+
switch (period) {
60+
case '1d':
61+
startDate.setDate(startDate.getDate() - 1)
62+
break
63+
case '7d':
64+
startDate.setDate(startDate.getDate() - 7)
65+
break
66+
case '30d':
67+
startDate.setDate(startDate.getDate() - 30)
68+
break
69+
}
70+
}
71+
72+
const result = await getUserUsageLogs(userId, {
73+
source: source as UsageLogSource | undefined,
74+
workspaceId,
75+
startDate,
76+
endDate,
77+
limit,
78+
cursor,
79+
})
80+
81+
logger.debug('Retrieved usage logs', {
82+
userId,
83+
source,
84+
period,
85+
logCount: result.logs.length,
86+
hasMore: result.pagination.hasMore,
87+
})
88+
89+
return NextResponse.json({
90+
success: true,
91+
...result,
92+
})
93+
} catch (error) {
94+
logger.error('Failed to get usage logs', {
95+
error: error instanceof Error ? error.message : String(error),
96+
})
97+
98+
return NextResponse.json(
99+
{
100+
error: 'Failed to retrieve usage logs',
101+
},
102+
{ status: 500 }
103+
)
104+
}
105+
}

apps/sim/app/api/v1/logs/filters.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ export interface LogFilters {
2525
export function buildLogFilters(filters: LogFilters): SQL<unknown> {
2626
const conditions: SQL<unknown>[] = []
2727

28-
// Required: workspace and permissions check
29-
conditions.push(eq(workflow.workspaceId, filters.workspaceId))
28+
conditions.push(eq(workflowExecutionLogs.workspaceId, filters.workspaceId))
3029

3130
// Cursor-based pagination
3231
if (filters.cursor) {

apps/sim/app/api/v1/logs/route.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ export async function GET(request: NextRequest) {
105105
const conditions = buildLogFilters(filters)
106106
const orderBy = getOrderBy(params.order)
107107

108-
// Build and execute query
109108
const baseQuery = db
110109
.select({
111110
id: workflowExecutionLogs.id,
@@ -124,13 +123,7 @@ export async function GET(request: NextRequest) {
124123
workflowDescription: workflow.description,
125124
})
126125
.from(workflowExecutionLogs)
127-
.innerJoin(
128-
workflow,
129-
and(
130-
eq(workflowExecutionLogs.workflowId, workflow.id),
131-
eq(workflow.workspaceId, params.workspaceId)
132-
)
133-
)
126+
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
134127
.innerJoin(
135128
permissions,
136129
and(
@@ -197,11 +190,8 @@ export async function GET(request: NextRequest) {
197190
return result
198191
})
199192

200-
// Get user's workflow execution limits and usage
201193
const limits = await getUserLimits(userId)
202194

203-
// Create response with limits information
204-
// The rateLimit object from checkRateLimit is for THIS API endpoint's rate limits
205195
const response = createApiResponse(
206196
{
207197
data: formattedLogs,

0 commit comments

Comments
 (0)