diff --git a/apps/sim/app/api/folders/[id]/route.ts b/apps/sim/app/api/folders/[id]/route.ts index cc25ecd770e..a4c4390b360 100644 --- a/apps/sim/app/api/folders/[id]/route.ts +++ b/apps/sim/app/api/folders/[id]/route.ts @@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' +import { captureServerEvent } from '@/lib/posthog/server' import { performDeleteFolder } from '@/lib/workflows/orchestration' import { checkForCircularReference } from '@/lib/workflows/utils' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -156,6 +157,13 @@ export async function DELETE( return NextResponse.json({ error: result.error }, { status }) } + captureServerEvent( + session.user.id, + 'folder_deleted', + { workspace_id: existingFolder.workspaceId }, + { groups: { workspace: existingFolder.workspaceId } } + ) + return NextResponse.json({ success: true, deletedItems: result.deletedItems, diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts index 2ae6d1673ab..a8106463d06 100644 --- a/apps/sim/app/api/folders/route.ts +++ b/apps/sim/app/api/folders/route.ts @@ -6,6 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' +import { captureServerEvent } from '@/lib/posthog/server' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' const logger = createLogger('FoldersAPI') @@ -145,6 +146,13 @@ export async function POST(request: NextRequest) { logger.info('Created new folder:', { id, name, workspaceId, parentId }) + captureServerEvent( + session.user.id, + 'folder_created', + { workspace_id: workspaceId }, + { groups: { workspace: workspaceId } } + ) + recordAudit({ workspaceId, actorId: session.user.id, diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts index c14d8e2681b..4f8735826b1 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts @@ -10,6 +10,7 @@ import { retryDocumentProcessing, updateDocument, } from '@/lib/knowledge/documents/service' +import { captureServerEvent } from '@/lib/posthog/server' import { checkDocumentAccess, checkDocumentWriteAccess } from '@/app/api/knowledge/utils' const logger = createLogger('DocumentByIdAPI') @@ -285,6 +286,14 @@ export async function DELETE( request: req, }) + const kbWorkspaceId = accessCheck.knowledgeBase?.workspaceId ?? '' + captureServerEvent( + userId, + 'knowledge_base_document_deleted', + { knowledge_base_id: knowledgeBaseId, workspace_id: kbWorkspaceId }, + kbWorkspaceId ? { groups: { workspace: kbWorkspaceId } } : undefined + ) + return NextResponse.json({ success: true, data: result, diff --git a/apps/sim/app/api/mothership/chats/[chatId]/route.ts b/apps/sim/app/api/mothership/chats/[chatId]/route.ts index 972651cf41b..3101e681589 100644 --- a/apps/sim/app/api/mothership/chats/[chatId]/route.ts +++ b/apps/sim/app/api/mothership/chats/[chatId]/route.ts @@ -159,16 +159,7 @@ export async function PATCH( } ) } - if (isUnread === false) { - captureServerEvent( - userId, - 'task_marked_read', - { workspace_id: updatedChat.workspaceId }, - { - groups: { workspace: updatedChat.workspaceId }, - } - ) - } else if (isUnread === true) { + if (isUnread === true) { captureServerEvent( userId, 'task_marked_unread', diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index 901e91392e8..597634aeb9d 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -7,6 +7,7 @@ import { z } from 'zod' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' +import { captureServerEvent } from '@/lib/posthog/server' import { validateCronExpression } from '@/lib/workflows/schedules/utils' import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' @@ -298,6 +299,13 @@ export async function DELETE( request, }) + captureServerEvent( + session.user.id, + 'scheduled_task_deleted', + { workspace_id: workspaceId ?? '' }, + workspaceId ? { groups: { workspace: workspaceId } } : undefined + ) + return NextResponse.json({ message: 'Schedule deleted successfully' }) } catch (error) { logger.error(`[${requestId}] Error deleting schedule`, error) diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts index 9c91530b985..19fd78bb18f 100644 --- a/apps/sim/app/api/schedules/route.ts +++ b/apps/sim/app/api/schedules/route.ts @@ -5,6 +5,7 @@ import { and, eq, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' +import { captureServerEvent } from '@/lib/posthog/server' import { validateCronExpression } from '@/lib/workflows/schedules/utils' import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' @@ -277,6 +278,13 @@ export async function POST(req: NextRequest) { lifecycle, }) + captureServerEvent( + session.user.id, + 'scheduled_task_created', + { workspace_id: workspaceId }, + { groups: { workspace: workspaceId } } + ) + return NextResponse.json( { schedule: { id, status: 'active', cronExpression, nextRunAt } }, { status: 201 } diff --git a/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts b/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts new file mode 100644 index 00000000000..0ab6c4aad8d --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts @@ -0,0 +1,96 @@ +import { + type AlarmType, + CloudWatchClient, + DescribeAlarmsCommand, + type StateValue, +} from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchDescribeAlarms') + +const DescribeAlarmsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + alarmNamePrefix: z.string().optional(), + stateValue: z.preprocess( + (v) => (v === '' ? undefined : v), + z.enum(['OK', 'ALARM', 'INSUFFICIENT_DATA']).optional() + ), + alarmType: z.preprocess( + (v) => (v === '' ? undefined : v), + z.enum(['MetricAlarm', 'CompositeAlarm']).optional() + ), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = DescribeAlarmsSchema.parse(body) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const command = new DescribeAlarmsCommand({ + ...(validatedData.alarmNamePrefix && { AlarmNamePrefix: validatedData.alarmNamePrefix }), + ...(validatedData.stateValue && { StateValue: validatedData.stateValue as StateValue }), + ...(validatedData.alarmType && { AlarmTypes: [validatedData.alarmType as AlarmType] }), + ...(validatedData.limit !== undefined && { MaxRecords: validatedData.limit }), + }) + + const response = await client.send(command) + + const metricAlarms = (response.MetricAlarms ?? []).map((a) => ({ + alarmName: a.AlarmName ?? '', + alarmArn: a.AlarmArn ?? '', + stateValue: a.StateValue ?? 'UNKNOWN', + stateReason: a.StateReason ?? '', + metricName: a.MetricName, + namespace: a.Namespace, + comparisonOperator: a.ComparisonOperator, + threshold: a.Threshold, + evaluationPeriods: a.EvaluationPeriods, + stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(), + })) + + const compositeAlarms = (response.CompositeAlarms ?? []).map((a) => ({ + alarmName: a.AlarmName ?? '', + alarmArn: a.AlarmArn ?? '', + stateValue: a.StateValue ?? 'UNKNOWN', + stateReason: a.StateReason ?? '', + metricName: undefined, + namespace: undefined, + comparisonOperator: undefined, + threshold: undefined, + evaluationPeriods: undefined, + stateUpdatedTimestamp: a.StateUpdatedTimestamp?.getTime(), + })) + + return NextResponse.json({ + success: true, + output: { alarms: [...metricAlarms, ...compositeAlarms] }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to describe CloudWatch alarms' + logger.error('DescribeAlarms failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/describe-log-groups/route.ts b/apps/sim/app/api/tools/cloudwatch/describe-log-groups/route.ts new file mode 100644 index 00000000000..a10f46c4efa --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/describe-log-groups/route.ts @@ -0,0 +1,62 @@ +import { DescribeLogGroupsCommand } from '@aws-sdk/client-cloudwatch-logs' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { createCloudWatchLogsClient } from '@/app/api/tools/cloudwatch/utils' + +const logger = createLogger('CloudWatchDescribeLogGroups') + +const DescribeLogGroupsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + prefix: z.string().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = DescribeLogGroupsSchema.parse(body) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + const command = new DescribeLogGroupsCommand({ + ...(validatedData.prefix && { logGroupNamePrefix: validatedData.prefix }), + ...(validatedData.limit !== undefined && { limit: validatedData.limit }), + }) + + const response = await client.send(command) + + const logGroups = (response.logGroups ?? []).map((lg) => ({ + logGroupName: lg.logGroupName ?? '', + arn: lg.arn ?? '', + storedBytes: lg.storedBytes ?? 0, + retentionInDays: lg.retentionInDays, + creationTime: lg.creationTime, + })) + + return NextResponse.json({ + success: true, + output: { logGroups }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to describe CloudWatch log groups' + logger.error('DescribeLogGroups failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/describe-log-streams/route.ts b/apps/sim/app/api/tools/cloudwatch/describe-log-streams/route.ts new file mode 100644 index 00000000000..5c9c69d7a8b --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/describe-log-streams/route.ts @@ -0,0 +1,53 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchDescribeLogStreams') + +import { createCloudWatchLogsClient, describeLogStreams } from '@/app/api/tools/cloudwatch/utils' + +const DescribeLogStreamsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + logGroupName: z.string().min(1, 'Log group name is required'), + prefix: z.string().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = DescribeLogStreamsSchema.parse(body) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + const result = await describeLogStreams(client, validatedData.logGroupName, { + prefix: validatedData.prefix, + limit: validatedData.limit, + }) + + return NextResponse.json({ + success: true, + output: { logStreams: result.logStreams }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to describe CloudWatch log streams' + logger.error('DescribeLogStreams failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/get-log-events/route.ts b/apps/sim/app/api/tools/cloudwatch/get-log-events/route.ts new file mode 100644 index 00000000000..78534999b7b --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/get-log-events/route.ts @@ -0,0 +1,61 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchGetLogEvents') + +import { createCloudWatchLogsClient, getLogEvents } from '@/app/api/tools/cloudwatch/utils' + +const GetLogEventsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + logGroupName: z.string().min(1, 'Log group name is required'), + logStreamName: z.string().min(1, 'Log stream name is required'), + startTime: z.number({ coerce: true }).int().optional(), + endTime: z.number({ coerce: true }).int().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = GetLogEventsSchema.parse(body) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + const result = await getLogEvents( + client, + validatedData.logGroupName, + validatedData.logStreamName, + { + startTime: validatedData.startTime, + endTime: validatedData.endTime, + limit: validatedData.limit, + } + ) + + return NextResponse.json({ + success: true, + output: { events: result.events }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to get CloudWatch log events' + logger.error('GetLogEvents failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts b/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts new file mode 100644 index 00000000000..1a510d3f12f --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts @@ -0,0 +1,97 @@ +import { CloudWatchClient, GetMetricStatisticsCommand } from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchGetMetricStatistics') + +const GetMetricStatisticsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + namespace: z.string().min(1, 'Namespace is required'), + metricName: z.string().min(1, 'Metric name is required'), + startTime: z.number({ coerce: true }).int(), + endTime: z.number({ coerce: true }).int(), + period: z.number({ coerce: true }).int().min(1), + statistics: z.array(z.enum(['Average', 'Sum', 'Minimum', 'Maximum', 'SampleCount'])).min(1), + dimensions: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = GetMetricStatisticsSchema.parse(body) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + let parsedDimensions: { Name: string; Value: string }[] | undefined + if (validatedData.dimensions) { + try { + const dims = JSON.parse(validatedData.dimensions) + if (Array.isArray(dims)) { + parsedDimensions = dims.map((d: Record) => ({ + Name: d.name, + Value: d.value, + })) + } else if (typeof dims === 'object') { + parsedDimensions = Object.entries(dims).map(([name, value]) => ({ + Name: name, + Value: String(value), + })) + } + } catch { + throw new Error('Invalid dimensions JSON') + } + } + + const command = new GetMetricStatisticsCommand({ + Namespace: validatedData.namespace, + MetricName: validatedData.metricName, + StartTime: new Date(validatedData.startTime * 1000), + EndTime: new Date(validatedData.endTime * 1000), + Period: validatedData.period, + Statistics: validatedData.statistics, + ...(parsedDimensions && { Dimensions: parsedDimensions }), + }) + + const response = await client.send(command) + + const datapoints = (response.Datapoints ?? []) + .sort((a, b) => (a.Timestamp?.getTime() ?? 0) - (b.Timestamp?.getTime() ?? 0)) + .map((dp) => ({ + timestamp: dp.Timestamp ? Math.floor(dp.Timestamp.getTime() / 1000) : 0, + average: dp.Average, + sum: dp.Sum, + minimum: dp.Minimum, + maximum: dp.Maximum, + sampleCount: dp.SampleCount, + unit: dp.Unit, + })) + + return NextResponse.json({ + success: true, + output: { + label: response.Label ?? validatedData.metricName, + datapoints, + }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to get CloudWatch metric statistics' + logger.error('GetMetricStatistics failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts b/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts new file mode 100644 index 00000000000..ce2cbf80f9f --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts @@ -0,0 +1,67 @@ +import { CloudWatchClient, ListMetricsCommand } from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchListMetrics') + +const ListMetricsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + namespace: z.string().optional(), + metricName: z.string().optional(), + recentlyActive: z.boolean().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = ListMetricsSchema.parse(body) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const command = new ListMetricsCommand({ + ...(validatedData.namespace && { Namespace: validatedData.namespace }), + ...(validatedData.metricName && { MetricName: validatedData.metricName }), + ...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }), + }) + + const response = await client.send(command) + + const metrics = (response.Metrics ?? []).slice(0, validatedData.limit ?? 500).map((m) => ({ + namespace: m.Namespace ?? '', + metricName: m.MetricName ?? '', + dimensions: (m.Dimensions ?? []).map((d) => ({ + name: d.Name ?? '', + value: d.Value ?? '', + })), + })) + + return NextResponse.json({ + success: true, + output: { metrics }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to list CloudWatch metrics' + logger.error('ListMetrics failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/query-logs/route.ts b/apps/sim/app/api/tools/cloudwatch/query-logs/route.ts new file mode 100644 index 00000000000..d5aa13e05c2 --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/query-logs/route.ts @@ -0,0 +1,72 @@ +import { StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchQueryLogs') + +import { createCloudWatchLogsClient, pollQueryResults } from '@/app/api/tools/cloudwatch/utils' + +const QueryLogsSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + logGroupNames: z.array(z.string().min(1)).min(1, 'At least one log group name is required'), + queryString: z.string().min(1, 'Query string is required'), + startTime: z.number({ coerce: true }).int(), + endTime: z.number({ coerce: true }).int(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.number({ coerce: true }).int().positive().optional() + ), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = QueryLogsSchema.parse(body) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + const startQueryCommand = new StartQueryCommand({ + logGroupNames: validatedData.logGroupNames, + queryString: validatedData.queryString, + startTime: validatedData.startTime, + endTime: validatedData.endTime, + ...(validatedData.limit !== undefined && { limit: validatedData.limit }), + }) + + const startQueryResponse = await client.send(startQueryCommand) + const queryId = startQueryResponse.queryId + + if (!queryId) { + throw new Error('Failed to start CloudWatch Log Insights query: no queryId returned') + } + + const result = await pollQueryResults(client, queryId) + + return NextResponse.json({ + success: true, + output: { + results: result.results, + statistics: result.statistics, + status: result.status, + }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'CloudWatch Log Insights query failed' + logger.error('QueryLogs failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/cloudwatch/utils.ts b/apps/sim/app/api/tools/cloudwatch/utils.ts new file mode 100644 index 00000000000..47db3b541da --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/utils.ts @@ -0,0 +1,161 @@ +import { + CloudWatchLogsClient, + DescribeLogStreamsCommand, + GetLogEventsCommand, + GetQueryResultsCommand, + type ResultField, +} from '@aws-sdk/client-cloudwatch-logs' +import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits' + +interface AwsCredentials { + region: string + accessKeyId: string + secretAccessKey: string +} + +export function createCloudWatchLogsClient(config: AwsCredentials): CloudWatchLogsClient { + return new CloudWatchLogsClient({ + region: config.region, + credentials: { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }, + }) +} + +interface PollOptions { + maxWaitMs?: number + pollIntervalMs?: number +} + +interface PollResult { + results: Record[] + statistics: { + bytesScanned: number + recordsMatched: number + recordsScanned: number + } + status: string +} + +function parseResultFields(fields: ResultField[] | undefined): Record { + const record: Record = {} + if (!fields) return record + for (const field of fields) { + if (field.field && field.value !== undefined) { + record[field.field] = field.value ?? '' + } + } + return record +} + +export async function pollQueryResults( + client: CloudWatchLogsClient, + queryId: string, + options: PollOptions = {} +): Promise { + const { maxWaitMs = DEFAULT_EXECUTION_TIMEOUT_MS, pollIntervalMs = 1_000 } = options + const startTime = Date.now() + + while (Date.now() - startTime < maxWaitMs) { + const command = new GetQueryResultsCommand({ queryId }) + const response = await client.send(command) + + const status = response.status ?? 'Unknown' + + if (status === 'Complete') { + return { + results: (response.results ?? []).map(parseResultFields), + statistics: { + bytesScanned: response.statistics?.bytesScanned ?? 0, + recordsMatched: response.statistics?.recordsMatched ?? 0, + recordsScanned: response.statistics?.recordsScanned ?? 0, + }, + status, + } + } + + if (status === 'Failed' || status === 'Cancelled') { + throw new Error(`CloudWatch Log Insights query ${status.toLowerCase()}`) + } + + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)) + } + + // Timeout -- fetch one last time for partial results + const finalResponse = await client.send(new GetQueryResultsCommand({ queryId })) + return { + results: (finalResponse.results ?? []).map(parseResultFields), + statistics: { + bytesScanned: finalResponse.statistics?.bytesScanned ?? 0, + recordsMatched: finalResponse.statistics?.recordsMatched ?? 0, + recordsScanned: finalResponse.statistics?.recordsScanned ?? 0, + }, + status: `Timeout (last status: ${finalResponse.status ?? 'Unknown'})`, + } +} + +export async function describeLogStreams( + client: CloudWatchLogsClient, + logGroupName: string, + options?: { prefix?: string; limit?: number } +): Promise<{ + logStreams: { + logStreamName: string + lastEventTimestamp: number | undefined + firstEventTimestamp: number | undefined + creationTime: number | undefined + storedBytes: number + }[] +}> { + const hasPrefix = Boolean(options?.prefix) + const command = new DescribeLogStreamsCommand({ + logGroupName, + ...(hasPrefix + ? { orderBy: 'LogStreamName', logStreamNamePrefix: options!.prefix } + : { orderBy: 'LastEventTime', descending: true }), + ...(options?.limit !== undefined && { limit: options.limit }), + }) + + const response = await client.send(command) + return { + logStreams: (response.logStreams ?? []).map((ls) => ({ + logStreamName: ls.logStreamName ?? '', + lastEventTimestamp: ls.lastEventTimestamp, + firstEventTimestamp: ls.firstEventTimestamp, + creationTime: ls.creationTime, + storedBytes: ls.storedBytes ?? 0, + })), + } +} + +export async function getLogEvents( + client: CloudWatchLogsClient, + logGroupName: string, + logStreamName: string, + options?: { startTime?: number; endTime?: number; limit?: number } +): Promise<{ + events: { + timestamp: number | undefined + message: string | undefined + ingestionTime: number | undefined + }[] +}> { + const command = new GetLogEventsCommand({ + logGroupIdentifier: logGroupName, + logStreamName, + ...(options?.startTime !== undefined && { startTime: options.startTime * 1000 }), + ...(options?.endTime !== undefined && { endTime: options.endTime * 1000 }), + ...(options?.limit !== undefined && { limit: options.limit }), + startFromHead: true, + }) + + const response = await client.send(command) + return { + events: (response.events ?? []).map((e) => ({ + timestamp: e.timestamp, + message: e.message, + ingestionTime: e.ingestionTime, + })), + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 5e98fc6e51b..e37fd8cff36 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -213,6 +213,7 @@ export function Home({ chatId }: HomeProps = {}) { if (!trimmed && !(fileAttachments && fileAttachments.length > 0)) return captureEvent(posthogRef.current, 'task_message_sent', { + workspace_id: workspaceId, has_attachments: !!(fileAttachments && fileAttachments.length > 0), has_contexts: !!(contexts && contexts.length > 0), is_new_task: !chatId, @@ -224,7 +225,7 @@ export function Home({ chatId }: HomeProps = {}) { sendMessage(trimmed || 'Analyze the attached file(s).', fileAttachments, contexts) }, - [sendMessage] + [sendMessage, workspaceId, chatId] ) useEffect(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx index 04926c08665..bcaf5d3019a 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx @@ -1,8 +1,9 @@ 'use client' -import { memo, useCallback, useMemo, useState } from 'react' +import { memo, useCallback, useMemo, useRef, useState } from 'react' import { ArrowUp, Bell, Library, MoreHorizontal, RefreshCw } from 'lucide-react' import { useParams } from 'next/navigation' +import { usePostHog } from 'posthog-js/react' import { useShallow } from 'zustand/react/shallow' import { Button, @@ -18,6 +19,7 @@ import { DatePicker } from '@/components/emcn/components/date-picker/date-picker import { cn } from '@/lib/core/utils/cn' import { hasActiveFilters } from '@/lib/logs/filters' import { getTriggerOptions } from '@/lib/logs/get-trigger-options' +import { captureEvent } from '@/lib/posthog/client' import { type LogStatus, STATUS_CONFIG } from '@/app/workspace/[workspaceId]/logs/utils' import { getBlock } from '@/blocks/registry' import { useFolderMap } from '@/hooks/queries/folders' @@ -179,6 +181,9 @@ export const LogsToolbar = memo(function LogsToolbar({ }: LogsToolbarProps) { const params = useParams() const workspaceId = params.workspaceId as string + const posthog = usePostHog() + const posthogRef = useRef(posthog) + posthogRef.current = posthog const { level, @@ -258,8 +263,45 @@ export const LogsToolbar = memo(function LogsToolbar({ } else { setLevel(values.join(',')) } + captureEvent(posthogRef.current, 'logs_filter_applied', { + filter_type: 'status', + workspace_id: workspaceId, + }) }, - [setLevel] + [setLevel, workspaceId] + ) + + const handleWorkflowFilterChange = useCallback( + (values: string[]) => { + setWorkflowIds(values) + captureEvent(posthogRef.current, 'logs_filter_applied', { + filter_type: 'workflow', + workspace_id: workspaceId, + }) + }, + [setWorkflowIds, workspaceId] + ) + + const handleFolderFilterChange = useCallback( + (values: string[]) => { + setFolderIds(values) + captureEvent(posthogRef.current, 'logs_filter_applied', { + filter_type: 'folder', + workspace_id: workspaceId, + }) + }, + [setFolderIds, workspaceId] + ) + + const handleTriggerFilterChange = useCallback( + (values: string[]) => { + setTriggers(values) + captureEvent(posthogRef.current, 'logs_filter_applied', { + filter_type: 'trigger', + workspace_id: workspaceId, + }) + }, + [setTriggers, workspaceId] ) const statusDisplayLabel = useMemo(() => { @@ -348,9 +390,13 @@ export const LogsToolbar = memo(function LogsToolbar({ } else { clearDateRange() setTimeRange(val as typeof timeRange) + captureEvent(posthogRef.current, 'logs_filter_applied', { + filter_type: 'time', + workspace_id: workspaceId, + }) } }, - [timeRange, setTimeRange, clearDateRange] + [timeRange, setTimeRange, clearDateRange, workspaceId] ) /** @@ -360,8 +406,12 @@ export const LogsToolbar = memo(function LogsToolbar({ (start: string, end: string) => { setDateRange(start, end) setDatePickerOpen(false) + captureEvent(posthogRef.current, 'logs_filter_applied', { + filter_type: 'time', + workspace_id: workspaceId, + }) }, - [setDateRange] + [setDateRange, workspaceId] ) /** @@ -545,7 +595,7 @@ export const LogsToolbar = memo(function LogsToolbar({ options={workflowOptions} multiSelect multiSelectValues={workflowIds} - onMultiSelectChange={setWorkflowIds} + onMultiSelectChange={handleWorkflowFilterChange} placeholder='All workflows' overlayContent={ @@ -580,7 +630,7 @@ export const LogsToolbar = memo(function LogsToolbar({ options={folderOptions} multiSelect multiSelectValues={folderIds} - onMultiSelectChange={setFolderIds} + onMultiSelectChange={handleFolderFilterChange} placeholder='All folders' overlayContent={ @@ -605,7 +655,7 @@ export const LogsToolbar = memo(function LogsToolbar({ options={triggerOptions} multiSelect multiSelectValues={triggers} - onMultiSelectChange={setTriggers} + onMultiSelectChange={handleTriggerFilterChange} placeholder='All triggers' overlayContent={ @@ -676,7 +726,7 @@ export const LogsToolbar = memo(function LogsToolbar({ options={workflowOptions} multiSelect multiSelectValues={workflowIds} - onMultiSelectChange={setWorkflowIds} + onMultiSelectChange={handleWorkflowFilterChange} placeholder='Workflow' overlayContent={ @@ -707,7 +757,7 @@ export const LogsToolbar = memo(function LogsToolbar({ options={folderOptions} multiSelect multiSelectValues={folderIds} - onMultiSelectChange={setFolderIds} + onMultiSelectChange={handleFolderFilterChange} placeholder='Folder' overlayContent={ {folderDisplayLabel} @@ -726,7 +776,7 @@ export const LogsToolbar = memo(function LogsToolbar({ options={triggerOptions} multiSelect multiSelectValues={triggers} - onMultiSelectChange={setTriggers} + onMultiSelectChange={handleTriggerFilterChange} placeholder='Trigger' overlayContent={ {triggerDisplayLabel} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 23c34bfdea4..3f54e933fa9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -3,11 +3,13 @@ import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' import { Command } from 'cmdk' import { useParams, useRouter } from 'next/navigation' +import { usePostHog } from 'posthog-js/react' import { createPortal } from 'react-dom' import { Library } from '@/components/emcn' import { Calendar, Database, File, HelpCircle, Settings, Table } from '@/components/emcn/icons' import { Search } from '@/components/emcn/icons/search' import { cn } from '@/lib/core/utils/cn' +import { captureEvent } from '@/lib/posthog/client' import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -55,11 +57,14 @@ export function SearchModal({ const [mounted, setMounted] = useState(false) const { navigateToSettings } = useSettingsNavigation() const { config: permissionConfig } = usePermissionConfig() + const posthog = usePostHog() const routerRef = useRef(router) routerRef.current = router const onOpenChangeRef = useRef(onOpenChange) onOpenChangeRef.current = onOpenChange + const posthogRef = useRef(posthog) + posthogRef.current = posthog useEffect(() => { setMounted(true) @@ -154,6 +159,8 @@ export function SearchModal({ }, [open]) const deferredSearch = useDeferredValue(search) + const deferredSearchRef = useRef(deferredSearch) + deferredSearchRef.current = deferredSearch const handleSearchChange = useCallback((value: string) => { setSearch(value) @@ -188,59 +195,151 @@ export function SearchModal({ detail: { type: block.type, enableTriggerMode }, }) ) + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: type, + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) onOpenChangeRef.current(false) }, - [] + [workspaceId] ) - const handleToolOperationSelect = useCallback((op: SearchToolOperationItem) => { - window.dispatchEvent( - new CustomEvent('add-block-from-toolbar', { - detail: { type: op.blockType, presetOperation: op.operationId }, - }) - ) - onOpenChangeRef.current(false) - }, []) - - const handleWorkflowSelect = useCallback((workflow: WorkflowItem) => { - if (!workflow.isCurrent && workflow.href) { - routerRef.current.push(workflow.href) + const handleToolOperationSelect = useCallback( + (op: SearchToolOperationItem) => { window.dispatchEvent( - new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: workflow.id } }) + new CustomEvent('add-block-from-toolbar', { + detail: { type: op.blockType, presetOperation: op.operationId }, + }) ) - } - onOpenChangeRef.current(false) - }, []) + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'tool_operation', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) - const handleWorkspaceSelect = useCallback((workspace: WorkspaceItem) => { - if (!workspace.isCurrent && workspace.href) { - routerRef.current.push(workspace.href) - } - onOpenChangeRef.current(false) - }, []) + const handleWorkflowSelect = useCallback( + (workflow: WorkflowItem) => { + if (!workflow.isCurrent && workflow.href) { + routerRef.current.push(workflow.href) + window.dispatchEvent( + new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: workflow.id } }) + ) + } + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'workflow', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) - const handleTaskSelect = useCallback((task: TaskItem) => { - routerRef.current.push(task.href) - onOpenChangeRef.current(false) - }, []) + const handleWorkspaceSelect = useCallback( + (workspace: WorkspaceItem) => { + if (!workspace.isCurrent && workspace.href) { + routerRef.current.push(workspace.href) + } + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'workspace', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) - const handlePageSelect = useCallback((page: PageItem) => { - if (page.onClick) { - page.onClick() - } else if (page.href) { - if (page.href.startsWith('http')) { - window.open(page.href, '_blank', 'noopener,noreferrer') - } else { - routerRef.current.push(page.href) + const handleTaskSelect = useCallback( + (task: TaskItem) => { + routerRef.current.push(task.href) + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'task', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) + + const handleTableSelect = useCallback( + (item: TaskItem) => { + routerRef.current.push(item.href) + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'table', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) + + const handleFileSelect = useCallback( + (item: TaskItem) => { + routerRef.current.push(item.href) + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'file', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) + + const handleKbSelect = useCallback( + (item: TaskItem) => { + routerRef.current.push(item.href) + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'knowledge_base', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) + + const handlePageSelect = useCallback( + (page: PageItem) => { + if (page.onClick) { + page.onClick() + } else if (page.href) { + if (page.href.startsWith('http')) { + window.open(page.href, '_blank', 'noopener,noreferrer') + } else { + routerRef.current.push(page.href) + } } - } - onOpenChangeRef.current(false) - }, []) + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'page', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) - const handleDocSelect = useCallback((doc: SearchDocItem) => { - window.open(doc.href, '_blank', 'noopener,noreferrer') - onOpenChangeRef.current(false) - }, []) + const handleDocSelect = useCallback( + (doc: SearchDocItem) => { + window.open(doc.href, '_blank', 'noopener,noreferrer') + captureEvent(posthogRef.current, 'search_result_selected', { + result_type: 'docs', + query_length: deferredSearchRef.current.length, + workspace_id: workspaceId, + }) + onOpenChangeRef.current(false) + }, + [workspaceId] + ) const handleBlockSelectAsBlock = useCallback( (block: SearchBlockItem) => handleBlockSelect(block, 'block'), @@ -366,9 +465,9 @@ export function SearchModal({ - - - + + + diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts index 91faca98627..d237ab3984a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-export-workflow.ts @@ -1,6 +1,8 @@ import { useCallback, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' +import { usePostHog } from 'posthog-js/react' +import { captureEvent } from '@/lib/posthog/client' import { downloadFile, exportWorkflowsToZip, @@ -27,6 +29,7 @@ export function useExportWorkflow({ onSuccess }: UseExportWorkflowProps = {}) { const [isExporting, setIsExporting] = useState(false) const params = useParams() const workspaceId = params.workspaceId as string | undefined + const posthog = usePostHog() const onSuccessRef = useRef(onSuccess) onSuccessRef.current = onSuccess @@ -34,6 +37,9 @@ export function useExportWorkflow({ onSuccess }: UseExportWorkflowProps = {}) { const workspaceIdRef = useRef(workspaceId) workspaceIdRef.current = workspaceId + const posthogRef = useRef(posthog) + posthogRef.current = posthog + /** * Export the workflow(s) to JSON or ZIP * - Single workflow: exports as JSON file @@ -100,6 +106,12 @@ export function useExportWorkflow({ onSuccess }: UseExportWorkflowProps = {}) { const { clearSelection } = useFolderStore.getState() clearSelection() + captureEvent(posthogRef.current, 'workflow_exported', { + workspace_id: workspaceIdRef.current ?? '', + workflow_count: exportedWorkflows.length, + format: exportedWorkflows.length === 1 ? 'json' : 'zip', + }) + logger.info('Workflow(s) exported successfully', { workflowIds: workflowIdsToExport, count: exportedWorkflows.length, diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts index 75ed0a4577f..ebce2c5312b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-import-workflow.ts @@ -1,7 +1,9 @@ -import { useCallback, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' import { useRouter } from 'next/navigation' +import { usePostHog } from 'posthog-js/react' +import { captureEvent } from '@/lib/posthog/client' import { extractWorkflowsFromFiles, extractWorkflowsFromZip, @@ -36,6 +38,9 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { const queryClient = useQueryClient() const createFolderMutation = useCreateFolder() const clearDiff = useWorkflowDiffStore((state) => state.clearDiff) + const posthog = usePostHog() + const posthogRef = useRef(posthog) + posthogRef.current = posthog const [isImporting, setIsImporting] = useState(false) /** @@ -204,6 +209,11 @@ export function useImportWorkflow({ workspaceId }: UseImportWorkflowProps) { logger.info(`Import complete. Imported ${importedWorkflowIds.length} workflow(s)`) if (importedWorkflowIds.length > 0) { + captureEvent(posthogRef.current, 'workflow_imported', { + workspace_id: workspaceId, + workflow_count: importedWorkflowIds.length, + format: hasZip && fileArray.length === 1 ? 'zip' : 'json', + }) router.push( `/workspace/${workspaceId}/w/${importedWorkflowIds[importedWorkflowIds.length - 1]}` ) diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts new file mode 100644 index 00000000000..c68bcf29430 --- /dev/null +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -0,0 +1,571 @@ +import { CloudWatchIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { IntegrationType } from '@/blocks/types' +import type { + CloudWatchDescribeAlarmsResponse, + CloudWatchDescribeLogGroupsResponse, + CloudWatchDescribeLogStreamsResponse, + CloudWatchGetLogEventsResponse, + CloudWatchGetMetricStatisticsResponse, + CloudWatchListMetricsResponse, + CloudWatchQueryLogsResponse, +} from '@/tools/cloudwatch/types' + +export const CloudWatchBlock: BlockConfig< + | CloudWatchQueryLogsResponse + | CloudWatchDescribeLogGroupsResponse + | CloudWatchDescribeLogStreamsResponse + | CloudWatchGetLogEventsResponse + | CloudWatchDescribeAlarmsResponse + | CloudWatchListMetricsResponse + | CloudWatchGetMetricStatisticsResponse +> = { + type: 'cloudwatch', + name: 'CloudWatch', + description: 'Query and monitor AWS CloudWatch logs, metrics, and alarms', + longDescription: + 'Integrate AWS CloudWatch into workflows. Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key.', + category: 'tools', + integrationType: IntegrationType.Analytics, + tags: ['cloud', 'monitoring'], + bgColor: 'linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)', + icon: CloudWatchIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Query Logs (Insights)', id: 'query_logs' }, + { label: 'Describe Log Groups', id: 'describe_log_groups' }, + { label: 'Get Log Events', id: 'get_log_events' }, + { label: 'Describe Log Streams', id: 'describe_log_streams' }, + { label: 'List Metrics', id: 'list_metrics' }, + { label: 'Get Metric Statistics', id: 'get_metric_statistics' }, + { label: 'Describe Alarms', id: 'describe_alarms' }, + ], + value: () => 'query_logs', + }, + { + id: 'awsRegion', + title: 'AWS Region', + type: 'short-input', + placeholder: 'us-east-1', + required: true, + }, + { + id: 'awsAccessKeyId', + title: 'AWS Access Key ID', + type: 'short-input', + placeholder: 'AKIA...', + password: true, + required: true, + }, + { + id: 'awsSecretAccessKey', + title: 'AWS Secret Access Key', + type: 'short-input', + placeholder: 'Your secret access key', + password: true, + required: true, + }, + // Query Logs fields + { + id: 'logGroupSelector', + title: 'Log Group', + type: 'file-selector', + canonicalParamId: 'logGroupNames', + selectorKey: 'cloudwatch.logGroups', + dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion'], + placeholder: 'Select a log group', + condition: { field: 'operation', value: 'query_logs' }, + required: { field: 'operation', value: 'query_logs' }, + mode: 'basic', + }, + { + id: 'logGroupNamesInput', + title: 'Log Group Names', + type: 'short-input', + canonicalParamId: 'logGroupNames', + placeholder: '/aws/lambda/my-func, /aws/ecs/my-service', + condition: { field: 'operation', value: 'query_logs' }, + required: { field: 'operation', value: 'query_logs' }, + mode: 'advanced', + }, + { + id: 'queryString', + title: 'Query', + type: 'code', + placeholder: 'fields @timestamp, @message\n| sort @timestamp desc\n| limit 20', + condition: { field: 'operation', value: 'query_logs' }, + required: { field: 'operation', value: 'query_logs' }, + wandConfig: { + enabled: true, + prompt: `Generate a CloudWatch Log Insights query based on the user's description. +The query language supports: fields, filter, stats, sort, limit, parse, display. +Common patterns: +- fields @timestamp, @message | sort @timestamp desc | limit 20 +- filter @message like /ERROR/ | stats count(*) by bin(1h) +- stats avg(duration) as avgDuration by functionName | sort avgDuration desc +- filter @message like /Exception/ | parse @message "* Exception: *" as prefix, errorMsg +- stats count(*) as requestCount by status | sort requestCount desc + +Return ONLY the query — no explanations, no markdown code blocks.`, + placeholder: 'Describe what you want to find in the logs...', + }, + }, + { + id: 'startTime', + title: 'Start Time (Unix epoch seconds)', + type: 'short-input', + placeholder: 'e.g., 1711900800', + condition: { + field: 'operation', + value: ['query_logs', 'get_log_events', 'get_metric_statistics'], + }, + required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] }, + }, + { + id: 'endTime', + title: 'End Time (Unix epoch seconds)', + type: 'short-input', + placeholder: 'e.g., 1711987200', + condition: { + field: 'operation', + value: ['query_logs', 'get_log_events', 'get_metric_statistics'], + }, + required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] }, + }, + // Describe Log Groups fields + { + id: 'prefix', + title: 'Log Group Name Prefix', + type: 'short-input', + placeholder: '/aws/lambda/', + condition: { field: 'operation', value: 'describe_log_groups' }, + }, + // Get Log Events / Describe Log Streams — shared log group selector + { + id: 'logGroupNameSelector', + title: 'Log Group', + type: 'file-selector', + canonicalParamId: 'logGroupName', + selectorKey: 'cloudwatch.logGroups', + dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion'], + placeholder: 'Select a log group', + condition: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + mode: 'basic', + }, + { + id: 'logGroupNameInput', + title: 'Log Group Name', + type: 'short-input', + canonicalParamId: 'logGroupName', + placeholder: '/aws/lambda/my-func', + condition: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + mode: 'advanced', + }, + // Describe Log Streams — stream prefix filter + { + id: 'streamPrefix', + title: 'Stream Name Prefix', + type: 'short-input', + placeholder: '2024/03/31/', + condition: { field: 'operation', value: 'describe_log_streams' }, + }, + // Get Log Events — log stream selector (cascading: depends on log group) + { + id: 'logStreamNameSelector', + title: 'Log Stream', + type: 'file-selector', + canonicalParamId: 'logStreamName', + selectorKey: 'cloudwatch.logStreams', + dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion', 'logGroupNameSelector'], + placeholder: 'Select a log stream', + condition: { field: 'operation', value: 'get_log_events' }, + required: { field: 'operation', value: 'get_log_events' }, + mode: 'basic', + }, + { + id: 'logStreamNameInput', + title: 'Log Stream Name', + type: 'short-input', + canonicalParamId: 'logStreamName', + placeholder: '2024/03/31/[$LATEST]abc123', + condition: { field: 'operation', value: 'get_log_events' }, + required: { field: 'operation', value: 'get_log_events' }, + mode: 'advanced', + }, + // List Metrics fields + { + id: 'metricNamespace', + title: 'Namespace', + type: 'short-input', + placeholder: 'e.g., AWS/EC2, AWS/Lambda, AWS/RDS', + condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] }, + required: { field: 'operation', value: 'get_metric_statistics' }, + }, + { + id: 'metricName', + title: 'Metric Name', + type: 'short-input', + placeholder: 'e.g., CPUUtilization, Invocations', + condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] }, + required: { field: 'operation', value: 'get_metric_statistics' }, + }, + { + id: 'recentlyActive', + title: 'Recently Active Only', + type: 'switch', + condition: { field: 'operation', value: 'list_metrics' }, + }, + // Get Metric Statistics fields + { + id: 'metricPeriod', + title: 'Period (seconds)', + type: 'short-input', + placeholder: 'e.g., 60, 300, 3600', + condition: { field: 'operation', value: 'get_metric_statistics' }, + required: { field: 'operation', value: 'get_metric_statistics' }, + }, + { + id: 'metricStatistics', + title: 'Statistics', + type: 'dropdown', + options: [ + { label: 'Average', id: 'Average' }, + { label: 'Sum', id: 'Sum' }, + { label: 'Minimum', id: 'Minimum' }, + { label: 'Maximum', id: 'Maximum' }, + { label: 'Sample Count', id: 'SampleCount' }, + ], + condition: { field: 'operation', value: 'get_metric_statistics' }, + required: { field: 'operation', value: 'get_metric_statistics' }, + }, + { + id: 'metricDimensions', + title: 'Dimensions', + type: 'table', + columns: ['name', 'value'], + condition: { field: 'operation', value: 'get_metric_statistics' }, + }, + // Describe Alarms fields + { + id: 'alarmNamePrefix', + title: 'Alarm Name Prefix', + type: 'short-input', + placeholder: 'my-service-', + condition: { field: 'operation', value: 'describe_alarms' }, + }, + { + id: 'stateValue', + title: 'State', + type: 'dropdown', + options: [ + { label: 'All States', id: '' }, + { label: 'OK', id: 'OK' }, + { label: 'ALARM', id: 'ALARM' }, + { label: 'INSUFFICIENT_DATA', id: 'INSUFFICIENT_DATA' }, + ], + condition: { field: 'operation', value: 'describe_alarms' }, + }, + { + id: 'alarmType', + title: 'Alarm Type', + type: 'dropdown', + options: [ + { label: 'All Types', id: '' }, + { label: 'Metric Alarm', id: 'MetricAlarm' }, + { label: 'Composite Alarm', id: 'CompositeAlarm' }, + ], + condition: { field: 'operation', value: 'describe_alarms' }, + }, + // Shared limit field + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: '100', + condition: { + field: 'operation', + value: [ + 'query_logs', + 'describe_log_groups', + 'get_log_events', + 'describe_log_streams', + 'list_metrics', + 'describe_alarms', + ], + }, + }, + ], + tools: { + access: [ + 'cloudwatch_query_logs', + 'cloudwatch_describe_log_groups', + 'cloudwatch_get_log_events', + 'cloudwatch_describe_log_streams', + 'cloudwatch_list_metrics', + 'cloudwatch_get_metric_statistics', + 'cloudwatch_describe_alarms', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'query_logs': + return 'cloudwatch_query_logs' + case 'describe_log_groups': + return 'cloudwatch_describe_log_groups' + case 'get_log_events': + return 'cloudwatch_get_log_events' + case 'describe_log_streams': + return 'cloudwatch_describe_log_streams' + case 'list_metrics': + return 'cloudwatch_list_metrics' + case 'get_metric_statistics': + return 'cloudwatch_get_metric_statistics' + case 'describe_alarms': + return 'cloudwatch_describe_alarms' + default: + throw new Error(`Invalid CloudWatch operation: ${params.operation}`) + } + }, + params: (params) => { + const { operation, startTime, endTime, limit, ...rest } = params + + const awsRegion = rest.awsRegion + const awsAccessKeyId = rest.awsAccessKeyId + const awsSecretAccessKey = rest.awsSecretAccessKey + const parsedLimit = limit ? Number.parseInt(String(limit), 10) : undefined + + switch (operation) { + case 'query_logs': { + const logGroupNames = rest.logGroupNames + if (!logGroupNames) { + throw new Error('Log group names are required') + } + if (!startTime) { + throw new Error('Start time is required') + } + if (!endTime) { + throw new Error('End time is required') + } + + const groupNames = + typeof logGroupNames === 'string' + ? logGroupNames + .split(',') + .map((n: string) => n.trim()) + .filter(Boolean) + : logGroupNames + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + logGroupNames: groupNames, + queryString: rest.queryString, + startTime: Number(startTime), + endTime: Number(endTime), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + } + + case 'describe_log_groups': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + ...(rest.prefix && { prefix: rest.prefix }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + + case 'get_log_events': { + if (!rest.logGroupName) { + throw new Error('Log group name is required') + } + if (!rest.logStreamName) { + throw new Error('Log stream name is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + logGroupName: rest.logGroupName, + logStreamName: rest.logStreamName, + ...(startTime && { startTime: Number(startTime) }), + ...(endTime && { endTime: Number(endTime) }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + } + + case 'describe_log_streams': { + if (!rest.logGroupName) { + throw new Error('Log group name is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + logGroupName: rest.logGroupName, + ...(rest.streamPrefix && { prefix: rest.streamPrefix }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + } + + case 'list_metrics': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + ...(rest.metricNamespace && { namespace: rest.metricNamespace }), + ...(rest.metricName && { metricName: rest.metricName }), + ...(rest.recentlyActive && { recentlyActive: true }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + + case 'get_metric_statistics': { + if (!rest.metricNamespace) { + throw new Error('Namespace is required') + } + if (!rest.metricName) { + throw new Error('Metric name is required') + } + if (!startTime) { + throw new Error('Start time is required') + } + if (!endTime) { + throw new Error('End time is required') + } + if (!rest.metricPeriod) { + throw new Error('Period is required') + } + + const stat = rest.metricStatistics + if (!stat) { + throw new Error('Statistics is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + namespace: rest.metricNamespace, + metricName: rest.metricName, + startTime: Number(startTime), + endTime: Number(endTime), + period: Number(rest.metricPeriod), + statistics: Array.isArray(stat) ? stat : [stat], + ...(rest.metricDimensions && { + dimensions: (() => { + const dims = rest.metricDimensions + if (typeof dims === 'string') return dims + if (Array.isArray(dims)) { + const obj: Record = {} + for (const row of dims) { + const name = row.cells?.name + const value = row.cells?.value + if (name && value !== undefined) obj[name] = String(value) + } + return JSON.stringify(obj) + } + return JSON.stringify(dims) + })(), + }), + } + } + + case 'describe_alarms': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + ...(rest.alarmNamePrefix && { alarmNamePrefix: rest.alarmNamePrefix }), + ...(rest.stateValue && { stateValue: rest.stateValue }), + ...(rest.alarmType && { alarmType: rest.alarmType }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + + default: + throw new Error(`Invalid CloudWatch operation: ${operation}`) + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'CloudWatch operation to perform' }, + awsRegion: { type: 'string', description: 'AWS region' }, + awsAccessKeyId: { type: 'string', description: 'AWS access key ID' }, + awsSecretAccessKey: { type: 'string', description: 'AWS secret access key' }, + logGroupNames: { type: 'string', description: 'Log group name(s) for query' }, + queryString: { type: 'string', description: 'CloudWatch Log Insights query string' }, + startTime: { type: 'string', description: 'Start time as Unix epoch seconds' }, + endTime: { type: 'string', description: 'End time as Unix epoch seconds' }, + prefix: { type: 'string', description: 'Log group name prefix filter' }, + logGroupName: { + type: 'string', + description: 'Log group name for get events / describe streams', + }, + logStreamName: { type: 'string', description: 'Log stream name for get events' }, + streamPrefix: { type: 'string', description: 'Log stream name prefix filter' }, + metricNamespace: { type: 'string', description: 'Metric namespace (e.g., AWS/EC2)' }, + metricName: { type: 'string', description: 'Metric name (e.g., CPUUtilization)' }, + recentlyActive: { type: 'boolean', description: 'Only show recently active metrics' }, + metricPeriod: { type: 'number', description: 'Granularity in seconds' }, + metricStatistics: { type: 'string', description: 'Statistic type (Average, Sum, etc.)' }, + metricDimensions: { type: 'json', description: 'Metric dimensions (Name/Value pairs)' }, + alarmNamePrefix: { type: 'string', description: 'Alarm name prefix filter' }, + stateValue: { + type: 'string', + description: 'Alarm state filter (OK, ALARM, INSUFFICIENT_DATA)', + }, + alarmType: { type: 'string', description: 'Alarm type filter (MetricAlarm, CompositeAlarm)' }, + limit: { type: 'number', description: 'Maximum number of results' }, + }, + outputs: { + results: { + type: 'array', + description: 'Log Insights query result rows', + }, + statistics: { + type: 'json', + description: 'Query statistics (bytesScanned, recordsMatched, recordsScanned)', + }, + status: { + type: 'string', + description: 'Query completion status', + }, + logGroups: { + type: 'array', + description: 'List of CloudWatch log groups', + }, + events: { + type: 'array', + description: 'Log events with timestamp and message', + }, + logStreams: { + type: 'array', + description: 'Log streams with metadata', + }, + metrics: { + type: 'array', + description: 'List of available metrics', + }, + label: { + type: 'string', + description: 'Metric label', + }, + datapoints: { + type: 'array', + description: 'Metric datapoints with timestamps and values', + }, + alarms: { + type: 'array', + description: 'CloudWatch alarms with state and configuration', + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 091408b7e99..c977522afd4 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -24,6 +24,7 @@ import { CirclebackBlock } from '@/blocks/blocks/circleback' import { ClayBlock } from '@/blocks/blocks/clay' import { ClerkBlock } from '@/blocks/blocks/clerk' import { CloudflareBlock } from '@/blocks/blocks/cloudflare' +import { CloudWatchBlock } from '@/blocks/blocks/cloudwatch' import { ConditionBlock } from '@/blocks/blocks/condition' import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence' import { CredentialBlock } from '@/blocks/blocks/credential' @@ -241,6 +242,7 @@ export const registry: Record = { chat_trigger: ChatTriggerBlock, circleback: CirclebackBlock, cloudflare: CloudflareBlock, + cloudwatch: CloudWatchBlock, clay: ClayBlock, clerk: ClerkBlock, condition: ConditionBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 49dda618ccb..25004d5da66 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4653,6 +4653,33 @@ export function SQSIcon(props: SVGProps) { ) } +export function CloudWatchIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function TextractIcon(props: SVGProps) { return ( = { })) }, }, + 'cloudwatch.logGroups': { + key: 'cloudwatch.logGroups', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'cloudwatch.logGroups', + context.awsAccessKeyId ?? 'none', + context.awsRegion ?? 'none', + ], + enabled: ({ context }) => + Boolean(context.awsAccessKeyId && context.awsSecretAccessKey && context.awsRegion), + fetchList: async ({ context, search }: SelectorQueryArgs) => { + const body = JSON.stringify({ + accessKeyId: context.awsAccessKeyId, + secretAccessKey: context.awsSecretAccessKey, + region: context.awsRegion, + ...(search && { prefix: search }), + }) + const data = await fetchJson<{ + output: { logGroups: { logGroupName: string }[] } + }>('/api/tools/cloudwatch/describe-log-groups', { + method: 'POST', + body, + }) + return (data.output?.logGroups || []).map((lg) => ({ + id: lg.logGroupName, + label: lg.logGroupName, + })) + }, + fetchById: async ({ detailId }: SelectorQueryArgs) => { + if (!detailId) return null + return { id: detailId, label: detailId } + }, + }, + 'cloudwatch.logStreams': { + key: 'cloudwatch.logStreams', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'cloudwatch.logStreams', + context.awsAccessKeyId ?? 'none', + context.awsRegion ?? 'none', + context.logGroupName ?? 'none', + ], + enabled: ({ context }) => + Boolean( + context.awsAccessKeyId && + context.awsSecretAccessKey && + context.awsRegion && + context.logGroupName + ), + fetchList: async ({ context, search }: SelectorQueryArgs) => { + const body = JSON.stringify({ + accessKeyId: context.awsAccessKeyId, + secretAccessKey: context.awsSecretAccessKey, + region: context.awsRegion, + logGroupName: context.logGroupName, + ...(search && { prefix: search }), + }) + const data = await fetchJson<{ + output: { logStreams: { logStreamName: string; lastEventTimestamp?: number }[] } + }>('/api/tools/cloudwatch/describe-log-streams', { + method: 'POST', + body, + }) + return (data.output?.logStreams || []).map((ls) => ({ + id: ls.logStreamName, + label: ls.logStreamName, + })) + }, + fetchById: async ({ detailId }: SelectorQueryArgs) => { + if (!detailId) return null + return { id: detailId, label: detailId } + }, + }, 'sim.workflows': { key: 'sim.workflows', staleTime: SELECTOR_STALE, diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index 5668d245226..bd5bcac547b 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -49,6 +49,8 @@ export type SelectorKey = | 'webflow.sites' | 'webflow.collections' | 'webflow.items' + | 'cloudwatch.logGroups' + | 'cloudwatch.logStreams' | 'sim.workflows' export interface SelectorOption { @@ -78,6 +80,10 @@ export interface SelectorContext { datasetId?: string serviceDeskId?: string impersonateUserEmail?: string + awsAccessKeyId?: string + awsSecretAccessKey?: string + awsRegion?: string + logGroupName?: string } export interface SelectorQueryArgs { diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index 30d4c052162..c186d82cd47 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -358,15 +358,12 @@ export interface PostHogEventMap { workspace_id: string } - task_marked_read: { - workspace_id: string - } - task_marked_unread: { workspace_id: string } task_message_sent: { + workspace_id: string has_attachments: boolean has_contexts: boolean is_new_task: boolean @@ -389,6 +386,62 @@ export interface PostHogEventMap { source: 'help_menu' | 'editor_button' | 'toolbar_context_menu' block_type?: string } + + search_result_selected: { + result_type: + | 'block' + | 'tool' + | 'trigger' + | 'tool_operation' + | 'workflow' + | 'workspace' + | 'task' + | 'table' + | 'file' + | 'knowledge_base' + | 'page' + | 'docs' + query_length: number + workspace_id: string + } + + workflow_imported: { + workspace_id: string + workflow_count: number + format: 'json' | 'zip' + } + + workflow_exported: { + workspace_id: string + workflow_count: number + format: 'json' | 'zip' + } + + folder_created: { + workspace_id: string + } + + folder_deleted: { + workspace_id: string + } + + logs_filter_applied: { + filter_type: 'status' | 'workflow' | 'folder' | 'trigger' | 'time' + workspace_id: string + } + + knowledge_base_document_deleted: { + knowledge_base_id: string + workspace_id: string + } + + scheduled_task_created: { + workspace_id: string + } + + scheduled_task_deleted: { + workspace_id: string + } } export type PostHogEventName = keyof PostHogEventMap diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index 4a1c22039a8..6f41759cffa 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -22,6 +22,10 @@ export const SELECTOR_CONTEXT_FIELDS = new Set([ 'datasetId', 'serviceDeskId', 'impersonateUserEmail', + 'awsAccessKeyId', + 'awsSecretAccessKey', + 'awsRegion', + 'logGroupName', ]) /** diff --git a/apps/sim/package.json b/apps/sim/package.json index 2b2ebded398..82d58472888 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -37,6 +37,8 @@ "@a2a-js/sdk": "0.3.7", "@anthropic-ai/sdk": "0.71.2", "@aws-sdk/client-bedrock-runtime": "3.940.0", + "@aws-sdk/client-cloudwatch": "3.940.0", + "@aws-sdk/client-cloudwatch-logs": "3.940.0", "@aws-sdk/client-dynamodb": "3.940.0", "@aws-sdk/client-rds-data": "3.940.0", "@aws-sdk/client-s3": "^3.779.0", diff --git a/apps/sim/tools/cloudwatch/describe_alarms.ts b/apps/sim/tools/cloudwatch/describe_alarms.ts new file mode 100644 index 00000000000..75913e76933 --- /dev/null +++ b/apps/sim/tools/cloudwatch/describe_alarms.ts @@ -0,0 +1,99 @@ +import type { + CloudWatchDescribeAlarmsParams, + CloudWatchDescribeAlarmsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const describeAlarmsTool: ToolConfig< + CloudWatchDescribeAlarmsParams, + CloudWatchDescribeAlarmsResponse +> = { + id: 'cloudwatch_describe_alarms', + name: 'CloudWatch Describe Alarms', + description: 'List and filter CloudWatch alarms', + version: '1.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + alarmNamePrefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter alarms by name prefix', + }, + stateValue: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by alarm state (OK, ALARM, INSUFFICIENT_DATA)', + }, + alarmType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by alarm type (MetricAlarm, CompositeAlarm)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of alarms to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/describe-alarms', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + ...(params.alarmNamePrefix && { alarmNamePrefix: params.alarmNamePrefix }), + ...(params.stateValue && { stateValue: params.stateValue }), + ...(params.alarmType && { alarmType: params.alarmType }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to describe CloudWatch alarms') + } + + return { + success: true, + output: { + alarms: data.output.alarms, + }, + } + }, + + outputs: { + alarms: { + type: 'array', + description: 'List of CloudWatch alarms with state and configuration', + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/describe_log_groups.ts b/apps/sim/tools/cloudwatch/describe_log_groups.ts new file mode 100644 index 00000000000..8cd885e039a --- /dev/null +++ b/apps/sim/tools/cloudwatch/describe_log_groups.ts @@ -0,0 +1,82 @@ +import type { + CloudWatchDescribeLogGroupsParams, + CloudWatchDescribeLogGroupsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const describeLogGroupsTool: ToolConfig< + CloudWatchDescribeLogGroupsParams, + CloudWatchDescribeLogGroupsResponse +> = { + id: 'cloudwatch_describe_log_groups', + name: 'CloudWatch Describe Log Groups', + description: 'List available CloudWatch log groups', + version: '1.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + prefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter log groups by name prefix', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of log groups to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/describe-log-groups', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + ...(params.prefix && { prefix: params.prefix }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to describe CloudWatch log groups') + } + + return { + success: true, + output: { + logGroups: data.output.logGroups, + }, + } + }, + + outputs: { + logGroups: { type: 'array', description: 'List of CloudWatch log groups with metadata' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/describe_log_streams.ts b/apps/sim/tools/cloudwatch/describe_log_streams.ts new file mode 100644 index 00000000000..4508704b0a2 --- /dev/null +++ b/apps/sim/tools/cloudwatch/describe_log_streams.ts @@ -0,0 +1,92 @@ +import type { + CloudWatchDescribeLogStreamsParams, + CloudWatchDescribeLogStreamsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const describeLogStreamsTool: ToolConfig< + CloudWatchDescribeLogStreamsParams, + CloudWatchDescribeLogStreamsResponse +> = { + id: 'cloudwatch_describe_log_streams', + name: 'CloudWatch Describe Log Streams', + description: 'List log streams within a CloudWatch log group', + version: '1.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + logGroupName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch log group name', + }, + prefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter log streams by name prefix', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of log streams to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/describe-log-streams', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + logGroupName: params.logGroupName, + ...(params.prefix && { prefix: params.prefix }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to describe CloudWatch log streams') + } + + return { + success: true, + output: { + logStreams: data.output.logStreams, + }, + } + }, + + outputs: { + logStreams: { + type: 'array', + description: 'List of log streams with metadata', + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/get_log_events.ts b/apps/sim/tools/cloudwatch/get_log_events.ts new file mode 100644 index 00000000000..0844feba02e --- /dev/null +++ b/apps/sim/tools/cloudwatch/get_log_events.ts @@ -0,0 +1,106 @@ +import type { + CloudWatchGetLogEventsParams, + CloudWatchGetLogEventsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const getLogEventsTool: ToolConfig< + CloudWatchGetLogEventsParams, + CloudWatchGetLogEventsResponse +> = { + id: 'cloudwatch_get_log_events', + name: 'CloudWatch Get Log Events', + description: 'Retrieve log events from a specific CloudWatch log stream', + version: '1.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + logGroupName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch log group name', + }, + logStreamName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch log stream name', + }, + startTime: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start time as Unix epoch seconds', + }, + endTime: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End time as Unix epoch seconds', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of events to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/get-log-events', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + logGroupName: params.logGroupName, + logStreamName: params.logStreamName, + ...(params.startTime !== undefined && { startTime: params.startTime }), + ...(params.endTime !== undefined && { endTime: params.endTime }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to get CloudWatch log events') + } + + return { + success: true, + output: { + events: data.output.events, + }, + } + }, + + outputs: { + events: { + type: 'array', + description: 'Log events with timestamp, message, and ingestion time', + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/get_metric_statistics.ts b/apps/sim/tools/cloudwatch/get_metric_statistics.ts new file mode 100644 index 00000000000..d9c4e2c59c0 --- /dev/null +++ b/apps/sim/tools/cloudwatch/get_metric_statistics.ts @@ -0,0 +1,119 @@ +import type { + CloudWatchGetMetricStatisticsParams, + CloudWatchGetMetricStatisticsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const getMetricStatisticsTool: ToolConfig< + CloudWatchGetMetricStatisticsParams, + CloudWatchGetMetricStatisticsResponse +> = { + id: 'cloudwatch_get_metric_statistics', + name: 'CloudWatch Get Metric Statistics', + description: 'Get statistics for a CloudWatch metric over a time range', + version: '1.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Metric namespace (e.g., AWS/EC2, AWS/Lambda)', + }, + metricName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Metric name (e.g., CPUUtilization, Invocations)', + }, + startTime: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Start time as Unix epoch seconds', + }, + endTime: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'End time as Unix epoch seconds', + }, + period: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Granularity in seconds (e.g., 60, 300, 3600)', + }, + statistics: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Statistics to retrieve (Average, Sum, Minimum, Maximum, SampleCount)', + }, + dimensions: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Dimensions as JSON (e.g., {"InstanceId": "i-1234"})', + }, + }, + + request: { + url: '/api/tools/cloudwatch/get-metric-statistics', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + namespace: params.namespace, + metricName: params.metricName, + startTime: params.startTime, + endTime: params.endTime, + period: params.period, + statistics: params.statistics, + ...(params.dimensions && { dimensions: params.dimensions }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to get CloudWatch metric statistics') + } + + return { + success: true, + output: { + label: data.output.label, + datapoints: data.output.datapoints, + }, + } + }, + + outputs: { + label: { type: 'string', description: 'Metric label' }, + datapoints: { type: 'array', description: 'Datapoints with timestamp and statistics values' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/index.ts b/apps/sim/tools/cloudwatch/index.ts new file mode 100644 index 00000000000..4ce796e168d --- /dev/null +++ b/apps/sim/tools/cloudwatch/index.ts @@ -0,0 +1,15 @@ +import { describeAlarmsTool } from '@/tools/cloudwatch/describe_alarms' +import { describeLogGroupsTool } from '@/tools/cloudwatch/describe_log_groups' +import { describeLogStreamsTool } from '@/tools/cloudwatch/describe_log_streams' +import { getLogEventsTool } from '@/tools/cloudwatch/get_log_events' +import { getMetricStatisticsTool } from '@/tools/cloudwatch/get_metric_statistics' +import { listMetricsTool } from '@/tools/cloudwatch/list_metrics' +import { queryLogsTool } from '@/tools/cloudwatch/query_logs' + +export const cloudwatchDescribeAlarmsTool = describeAlarmsTool +export const cloudwatchDescribeLogGroupsTool = describeLogGroupsTool +export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool +export const cloudwatchGetLogEventsTool = getLogEventsTool +export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool +export const cloudwatchListMetricsTool = listMetricsTool +export const cloudwatchQueryLogsTool = queryLogsTool diff --git a/apps/sim/tools/cloudwatch/list_metrics.ts b/apps/sim/tools/cloudwatch/list_metrics.ts new file mode 100644 index 00000000000..eb4754c0459 --- /dev/null +++ b/apps/sim/tools/cloudwatch/list_metrics.ts @@ -0,0 +1,96 @@ +import type { + CloudWatchListMetricsParams, + CloudWatchListMetricsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const listMetricsTool: ToolConfig< + CloudWatchListMetricsParams, + CloudWatchListMetricsResponse +> = { + id: 'cloudwatch_list_metrics', + name: 'CloudWatch List Metrics', + description: 'List available CloudWatch metrics', + version: '1.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + namespace: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by namespace (e.g., AWS/EC2, AWS/Lambda)', + }, + metricName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by metric name', + }, + recentlyActive: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Only show metrics active in the last 3 hours', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of metrics to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/list-metrics', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + ...(params.namespace && { namespace: params.namespace }), + ...(params.metricName && { metricName: params.metricName }), + ...(params.recentlyActive && { recentlyActive: params.recentlyActive }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to list CloudWatch metrics') + } + + return { + success: true, + output: { + metrics: data.output.metrics, + }, + } + }, + + outputs: { + metrics: { type: 'array', description: 'List of metrics with namespace, name, and dimensions' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/query_logs.ts b/apps/sim/tools/cloudwatch/query_logs.ts new file mode 100644 index 00000000000..3031ff471db --- /dev/null +++ b/apps/sim/tools/cloudwatch/query_logs.ts @@ -0,0 +1,107 @@ +import type { + CloudWatchQueryLogsParams, + CloudWatchQueryLogsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const queryLogsTool: ToolConfig = { + id: 'cloudwatch_query_logs', + name: 'CloudWatch Query Logs', + description: 'Run a CloudWatch Log Insights query against one or more log groups', + version: '1.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + logGroupNames: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: 'Log group names to query', + }, + queryString: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch Log Insights query string', + }, + startTime: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Start time as Unix epoch seconds', + }, + endTime: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'End time as Unix epoch seconds', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/query-logs', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + logGroupNames: params.logGroupNames, + queryString: params.queryString, + startTime: params.startTime, + endTime: params.endTime, + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'CloudWatch Log Insights query failed') + } + + return { + success: true, + output: { + results: data.output.results, + statistics: data.output.statistics, + status: data.output.status, + }, + } + }, + + outputs: { + results: { type: 'array', description: 'Query result rows' }, + statistics: { + type: 'object', + description: 'Query statistics (bytesScanned, recordsMatched, recordsScanned)', + }, + status: { type: 'string', description: 'Query completion status' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/types.ts b/apps/sim/tools/cloudwatch/types.ts new file mode 100644 index 00000000000..32259fa5a08 --- /dev/null +++ b/apps/sim/tools/cloudwatch/types.ts @@ -0,0 +1,146 @@ +import type { ToolResponse } from '@/tools/types' + +export interface CloudWatchConnectionConfig { + awsRegion: string + awsAccessKeyId: string + awsSecretAccessKey: string +} + +export interface CloudWatchQueryLogsParams extends CloudWatchConnectionConfig { + logGroupNames: string[] + queryString: string + startTime: number + endTime: number + limit?: number +} + +export interface CloudWatchDescribeLogGroupsParams extends CloudWatchConnectionConfig { + prefix?: string + limit?: number +} + +export interface CloudWatchGetLogEventsParams extends CloudWatchConnectionConfig { + logGroupName: string + logStreamName: string + startTime?: number + endTime?: number + limit?: number +} + +export interface CloudWatchQueryLogsResponse extends ToolResponse { + output: { + results: Record[] + statistics: { + bytesScanned: number + recordsMatched: number + recordsScanned: number + } + status: string + } +} + +export interface CloudWatchDescribeLogGroupsResponse extends ToolResponse { + output: { + logGroups: { + logGroupName: string + arn: string + storedBytes: number + retentionInDays: number | undefined + creationTime: number | undefined + }[] + } +} + +export interface CloudWatchGetLogEventsResponse extends ToolResponse { + output: { + events: { + timestamp: number | undefined + message: string | undefined + ingestionTime: number | undefined + }[] + } +} + +export interface CloudWatchDescribeLogStreamsParams extends CloudWatchConnectionConfig { + logGroupName: string + prefix?: string + limit?: number +} + +export interface CloudWatchDescribeLogStreamsResponse extends ToolResponse { + output: { + logStreams: { + logStreamName: string + lastEventTimestamp: number | undefined + firstEventTimestamp: number | undefined + creationTime: number | undefined + storedBytes: number + }[] + } +} + +export interface CloudWatchListMetricsParams extends CloudWatchConnectionConfig { + namespace?: string + metricName?: string + recentlyActive?: boolean + limit?: number +} + +export interface CloudWatchListMetricsResponse extends ToolResponse { + output: { + metrics: { + namespace: string + metricName: string + dimensions: { name: string; value: string }[] + }[] + } +} + +export interface CloudWatchGetMetricStatisticsParams extends CloudWatchConnectionConfig { + namespace: string + metricName: string + startTime: number + endTime: number + period: number + statistics: string[] + dimensions?: string +} + +export interface CloudWatchGetMetricStatisticsResponse extends ToolResponse { + output: { + label: string + datapoints: { + timestamp: number + average?: number + sum?: number + minimum?: number + maximum?: number + sampleCount?: number + unit?: string + }[] + } +} + +export interface CloudWatchDescribeAlarmsParams extends CloudWatchConnectionConfig { + alarmNamePrefix?: string + stateValue?: string + alarmType?: string + limit?: number +} + +export interface CloudWatchDescribeAlarmsResponse extends ToolResponse { + output: { + alarms: { + alarmName: string + alarmArn: string + stateValue: string + stateReason: string + metricName: string | undefined + namespace: string | undefined + comparisonOperator: string | undefined + threshold: number | undefined + evaluationPeriods: number | undefined + stateUpdatedTimestamp: number | undefined + }[] + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 7508d98dac5..3dfbabf2a19 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -275,6 +275,15 @@ import { cloudflareUpdateDnsRecordTool, cloudflareUpdateZoneSettingTool, } from '@/tools/cloudflare' +import { + cloudwatchDescribeAlarmsTool, + cloudwatchDescribeLogGroupsTool, + cloudwatchDescribeLogStreamsTool, + cloudwatchGetLogEventsTool, + cloudwatchGetMetricStatisticsTool, + cloudwatchListMetricsTool, + cloudwatchQueryLogsTool, +} from '@/tools/cloudwatch' import { confluenceAddLabelTool, confluenceCreateBlogPostTool, @@ -3376,6 +3385,13 @@ export const tools: Record = { rds_delete: rdsDeleteTool, rds_execute: rdsExecuteTool, rds_introspect: rdsIntrospectTool, + cloudwatch_query_logs: cloudwatchQueryLogsTool, + cloudwatch_describe_log_groups: cloudwatchDescribeLogGroupsTool, + cloudwatch_describe_alarms: cloudwatchDescribeAlarmsTool, + cloudwatch_describe_log_streams: cloudwatchDescribeLogStreamsTool, + cloudwatch_get_log_events: cloudwatchGetLogEventsTool, + cloudwatch_list_metrics: cloudwatchListMetricsTool, + cloudwatch_get_metric_statistics: cloudwatchGetMetricStatisticsTool, dynamodb_get: dynamodbGetTool, dynamodb_put: dynamodbPutTool, dynamodb_query: dynamodbQueryTool, diff --git a/bun.lock b/bun.lock index a9edfffb951..9a62972f278 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,8 @@ "@a2a-js/sdk": "0.3.7", "@anthropic-ai/sdk": "0.71.2", "@aws-sdk/client-bedrock-runtime": "3.940.0", + "@aws-sdk/client-cloudwatch": "3.940.0", + "@aws-sdk/client-cloudwatch-logs": "3.940.0", "@aws-sdk/client-dynamodb": "3.940.0", "@aws-sdk/client-rds-data": "3.940.0", "@aws-sdk/client-s3": "^3.779.0", @@ -414,6 +416,10 @@ "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/eventstream-handler-node": "3.936.0", "@aws-sdk/middleware-eventstream": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/middleware-websocket": "3.936.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/token-providers": "3.940.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Gs6UUQP1zt8vahOxJ3BADcb3B+2KldUNA3bKa+KdK58de7N7tLJFJfZuXhFGGtwyNPh1aw6phtdP6dauq3OLWA=="], + "@aws-sdk/client-cloudwatch": ["@aws-sdk/client-cloudwatch@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-compression": "^4.3.12", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-C35xpPntRAGdEg3X5iKpSUCBaP3yxYNo1U95qipN/X1e0/TYIDWHwGt8Z1ntRafK19jp5oVzhRQ+PD1JAPSEzA=="], + + "@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7dEIO3D98IxA9IhqixPJbzQsBkk4TchHHpFdd0JOhlSlihWhiwbf3ijUePJVXYJxcpRRtMmAMtDRLDzCSO+ZHg=="], + "@aws-sdk/client-dynamodb": ["@aws-sdk/client-dynamodb@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-endpoint-discovery": "3.936.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-u2sXsNJazJbuHeWICvsj6RvNyJh3isedEfPvB21jK/kxcriK+dE/izlKC2cyxUjERCmku0zTFNzY9FhrLbYHjQ=="], "@aws-sdk/client-rds-data": ["@aws-sdk/client-rds-data@3.940.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.940.0", "@aws-sdk/credential-provider-node": "3.940.0", "@aws-sdk/middleware-host-header": "3.936.0", "@aws-sdk/middleware-logger": "3.936.0", "@aws-sdk/middleware-recursion-detection": "3.936.0", "@aws-sdk/middleware-user-agent": "3.940.0", "@aws-sdk/region-config-resolver": "3.936.0", "@aws-sdk/types": "3.936.0", "@aws-sdk/util-endpoints": "3.936.0", "@aws-sdk/util-user-agent-browser": "3.936.0", "@aws-sdk/util-user-agent-node": "3.940.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.12", "@smithy/middleware-retry": "^4.4.12", "@smithy/middleware-serde": "^4.2.6", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.8", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.11", "@smithy/util-defaults-mode-node": "^4.2.14", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-68NH61MvS48CVPfzBNCPdCG4KnNjM+Uj/3DSw7rT9PJvdML9ARS4M2Uqco9POPw+Aj20KBumsEUd6FMVcYBXAA=="], @@ -1348,6 +1354,8 @@ "@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="], + "@smithy/middleware-compression": ["@smithy/middleware-compression@4.3.42", "", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "fflate": "0.8.1", "tslib": "^2.6.2" } }, "sha512-Ys2R8N7oZ3b6p063lhk7paRbX1F9Ju8a8Bsrw2nFfsG8iHYpgfW6ijd7hJKqRe+Wq9ABfcmX3luBlEd+B5/jVA=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="], "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="], @@ -4122,6 +4130,10 @@ "@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="], + "@smithy/middleware-compression/@smithy/core": ["@smithy/core@3.23.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q=="], + + "@smithy/middleware-compression/fflate": ["fflate@0.8.1", "", {}, "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ=="], + "@socket.io/redis-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -4662,6 +4674,8 @@ "@shikijs/rehype/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + "@smithy/middleware-compression/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="], + "@trigger.dev/core/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="], @@ -5130,6 +5144,8 @@ "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "@smithy/middleware-compression/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.1", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw=="], + "@trigger.dev/core/socket.io-client/engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], "@trigger.dev/core/socket.io-client/engine.io-client/xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.0.0", "", {}, "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="],