Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion apps/sim/app/api/auth/accounts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,17 @@ export async function GET(request: NextRequest) {
.from(account)
.where(and(...whereConditions))

return NextResponse.json({ accounts })
// Use the user's email as the display name (consistent with credential selector)
const userEmail = session.user.email

const accountsWithDisplayName = accounts.map((acc) => ({
id: acc.id,
accountId: acc.accountId,
providerId: acc.providerId,
displayName: userEmail || acc.providerId,
}))

return NextResponse.json({ accounts: accountsWithDisplayName })
} catch (error) {
logger.error('Failed to fetch accounts', { error })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { processInputFileFields } from '@/lib/execution/files'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
Expand All @@ -30,7 +31,7 @@ const logger = createLogger('WorkflowExecuteAPI')

const ExecuteWorkflowSchema = z.object({
selectedOutputs: z.array(z.string()).optional().default([]),
triggerType: z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']).optional(),
triggerType: z.enum(ALL_TRIGGER_TYPES).optional(),
stream: z.boolean().optional(),
useDraftState: z.boolean().optional(),
input: z.any().optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { createLogger } from '@/lib/logs/console/logger'
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'

const logger = createLogger('WorkspaceNotificationAPI')

const levelFilterSchema = z.array(z.enum(['info', 'error']))
const triggerFilterSchema = z.array(z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']))
const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))

const alertRuleSchema = z.enum([
'consecutive_failures',
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/workspaces/[id]/notifications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { createLogger } from '@/lib/logs/console/logger'
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'

const logger = createLogger('WorkspaceNotificationsAPI')

const notificationTypeSchema = z.enum(['webhook', 'email', 'slack'])
const levelFilterSchema = z.array(z.enum(['info', 'error']))
const triggerFilterSchema = z.array(z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']))
const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))

const alertRuleSchema = z.enum([
'consecutive_failures',
Expand Down Expand Up @@ -80,7 +81,7 @@ const createNotificationSchema = z
workflowIds: z.array(z.string()).max(MAX_WORKFLOW_IDS).default([]),
allWorkflows: z.boolean().default(false),
levelFilter: levelFilterSchema.default(['info', 'error']),
triggerFilter: triggerFilterSchema.default(['api', 'webhook', 'schedule', 'manual', 'chat']),
triggerFilter: triggerFilterSchema.default([...ALL_TRIGGER_TYPES]),
includeFinalOutput: z.boolean().default(false),
includeTraceSpans: z.boolean().default(false),
includeRateLimits: z.boolean().default(false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export function SlackChannelSelector({
disabled={disabled || channels.length === 0}
isLoading={isLoading}
error={fetchError}
searchable
searchPlaceholder='Search channels...'
/>
{selectedChannel && !fetchError && (
<p className='text-[12px] text-[var(--text-muted)]'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { SlackIcon } from '@/components/icons'
import { Skeleton } from '@/components/ui'
import { cn } from '@/lib/core/utils/cn'
import { createLogger } from '@/lib/logs/console/logger'
import { ALL_TRIGGER_TYPES, type TriggerType } from '@/lib/logs/types'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import {
type NotificationSubscription,
Expand All @@ -43,7 +44,6 @@ const PRIMARY_BUTTON_STYLES =

type NotificationType = 'webhook' | 'email' | 'slack'
type LogLevel = 'info' | 'error'
type TriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
type AlertRule =
| 'none'
| 'consecutive_failures'
Expand Down Expand Up @@ -84,7 +84,6 @@ interface NotificationSettingsProps {
}

const LOG_LEVELS: LogLevel[] = ['info', 'error']
const TRIGGER_TYPES: TriggerType[] = ['api', 'webhook', 'schedule', 'manual', 'chat']

function formatAlertConfigLabel(config: {
rule: AlertRule
Expand Down Expand Up @@ -137,7 +136,7 @@ export function NotificationSettings({
workflowIds: [] as string[],
allWorkflows: true,
levelFilter: ['info', 'error'] as LogLevel[],
triggerFilter: ['api', 'webhook', 'schedule', 'manual', 'chat'] as TriggerType[],
triggerFilter: [...ALL_TRIGGER_TYPES] as TriggerType[],
includeFinalOutput: false,
includeTraceSpans: false,
includeRateLimits: false,
Expand Down Expand Up @@ -207,7 +206,7 @@ export function NotificationSettings({
workflowIds: [],
allWorkflows: true,
levelFilter: ['info', 'error'],
triggerFilter: ['api', 'webhook', 'schedule', 'manual', 'chat'],
triggerFilter: [...ALL_TRIGGER_TYPES],
includeFinalOutput: false,
includeTraceSpans: false,
includeRateLimits: false,
Expand Down Expand Up @@ -768,7 +767,7 @@ export function NotificationSettings({
<Combobox
options={slackAccounts.map((acc) => ({
value: acc.id,
label: acc.accountId,
label: acc.displayName || 'Slack Workspace',
}))}
value={formData.slackAccountId}
onChange={(value) => {
Expand Down Expand Up @@ -859,7 +858,7 @@ export function NotificationSettings({
<div className='flex flex-col gap-[8px]'>
<Label className='text-[var(--text-secondary)]'>Trigger Type Filters</Label>
<Combobox
options={TRIGGER_TYPES.map((trigger) => ({
options={ALL_TRIGGER_TYPES.map((trigger) => ({
label: trigger.charAt(0).toUpperCase() + trigger.slice(1),
value: trigger,
}))}
Expand Down
1 change: 1 addition & 0 deletions apps/sim/hooks/use-slack-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ interface SlackAccount {
id: string
accountId: string
providerId: string
displayName?: string
}

interface UseSlackAccountsResult {
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/lib/logs/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ export async function emitWorkflowExecutionCompleted(log: WorkflowExecutionLog):
)

for (const subscription of subscriptions) {
const levelMatches = subscription.levelFilter?.includes(log.level) ?? true
const triggerMatches = subscription.triggerFilter?.includes(log.trigger) ?? true
const levelMatches = subscription.levelFilter.includes(log.level)
const triggerMatches = subscription.triggerFilter.includes(log.trigger)

if (!levelMatches || !triggerMatches) {
logger.debug(`Skipping subscription ${subscription.id} due to filter mismatch`)
Expand All @@ -98,6 +98,7 @@ export async function emitWorkflowExecutionCompleted(log: WorkflowExecutionLog):
status: log.level === 'error' ? 'error' : 'success',
durationMs: log.totalDurationMs || 0,
cost: (log.cost as { total?: number })?.total || 0,
triggerFilter: subscription.triggerFilter,
}

const shouldAlert = await shouldTriggerAlert(alertConfig, context, subscription.lastAlertAt)
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/lib/logs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ export interface ExecutionEnvironment {
workspaceId: string
}

export const ALL_TRIGGER_TYPES = ['api', 'webhook', 'schedule', 'manual', 'chat'] as const
export type TriggerType = (typeof ALL_TRIGGER_TYPES)[number]

export interface ExecutionTrigger {
type: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | string
type: TriggerType | string
source: string
data?: Record<string, unknown>
timestamp: string
Expand Down
74 changes: 41 additions & 33 deletions apps/sim/lib/notifications/alert-rules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { db } from '@sim/db'
import { workflowExecutionLogs } from '@sim/db/schema'
import { and, avg, count, desc, eq, gte } from 'drizzle-orm'
import { and, avg, count, desc, eq, gte, inArray } from 'drizzle-orm'
import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('AlertRules')
Expand Down Expand Up @@ -135,25 +135,29 @@ export function isInCooldown(lastAlertAt: Date | null): boolean {
return new Date() < cooldownEnd
}

/**
* Context passed to alert check functions
*/
export interface AlertCheckContext {
workflowId: string
executionId: string
status: 'success' | 'error'
durationMs: number
cost: number
triggerFilter: string[]
}

/**
* Check if consecutive failures threshold is met
*/
async function checkConsecutiveFailures(workflowId: string, threshold: number): Promise<boolean> {
async function checkConsecutiveFailures(
workflowId: string,
threshold: number,
triggerFilter: string[]
): Promise<boolean> {
const recentLogs = await db
.select({ level: workflowExecutionLogs.level })
.from(workflowExecutionLogs)
.where(eq(workflowExecutionLogs.workflowId, workflowId))
.where(
and(
eq(workflowExecutionLogs.workflowId, workflowId),
inArray(workflowExecutionLogs.trigger, triggerFilter)
)
)
.orderBy(desc(workflowExecutionLogs.createdAt))
.limit(threshold)

Expand All @@ -162,13 +166,11 @@ async function checkConsecutiveFailures(workflowId: string, threshold: number):
return recentLogs.every((log) => log.level === 'error')
}

/**
* Check if failure rate exceeds threshold
*/
async function checkFailureRate(
workflowId: string,
ratePercent: number,
windowHours: number
windowHours: number,
triggerFilter: string[]
): Promise<boolean> {
const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000)

Expand All @@ -181,7 +183,8 @@ async function checkFailureRate(
.where(
and(
eq(workflowExecutionLogs.workflowId, workflowId),
gte(workflowExecutionLogs.createdAt, windowStart)
gte(workflowExecutionLogs.createdAt, windowStart),
inArray(workflowExecutionLogs.trigger, triggerFilter)
)
)
.orderBy(workflowExecutionLogs.createdAt)
Expand All @@ -206,14 +209,12 @@ function checkLatencyThreshold(durationMs: number, thresholdMs: number): boolean
return durationMs > thresholdMs
}

/**
* Check if execution duration is significantly above average
*/
async function checkLatencySpike(
workflowId: string,
currentDurationMs: number,
spikePercent: number,
windowHours: number
windowHours: number,
triggerFilter: string[]
): Promise<boolean> {
const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000)

Expand All @@ -226,7 +227,8 @@ async function checkLatencySpike(
.where(
and(
eq(workflowExecutionLogs.workflowId, workflowId),
gte(workflowExecutionLogs.createdAt, windowStart)
gte(workflowExecutionLogs.createdAt, windowStart),
inArray(workflowExecutionLogs.trigger, triggerFilter)
)
)

Expand All @@ -248,13 +250,11 @@ function checkCostThreshold(cost: number, thresholdDollars: number): boolean {
return cost > thresholdDollars
}

/**
* Check if error count exceeds threshold within window
*/
async function checkErrorCount(
workflowId: string,
threshold: number,
windowHours: number
windowHours: number,
triggerFilter: string[]
): Promise<boolean> {
const windowStart = new Date(Date.now() - windowHours * 60 * 60 * 1000)

Expand All @@ -265,17 +265,15 @@ async function checkErrorCount(
and(
eq(workflowExecutionLogs.workflowId, workflowId),
eq(workflowExecutionLogs.level, 'error'),
gte(workflowExecutionLogs.createdAt, windowStart)
gte(workflowExecutionLogs.createdAt, windowStart),
inArray(workflowExecutionLogs.trigger, triggerFilter)
)
)

const errorCount = result[0]?.count || 0
return errorCount >= threshold
}

/**
* Evaluates if an alert should be triggered based on the configuration
*/
export async function shouldTriggerAlert(
config: AlertConfig,
context: AlertCheckContext,
Expand All @@ -287,16 +285,21 @@ export async function shouldTriggerAlert(
}

const { rule } = config
const { workflowId, status, durationMs, cost } = context
const { workflowId, status, durationMs, cost, triggerFilter } = context

switch (rule) {
case 'consecutive_failures':
if (status !== 'error') return false
return checkConsecutiveFailures(workflowId, config.consecutiveFailures!)
return checkConsecutiveFailures(workflowId, config.consecutiveFailures!, triggerFilter)

case 'failure_rate':
if (status !== 'error') return false
return checkFailureRate(workflowId, config.failureRatePercent!, config.windowHours!)
return checkFailureRate(
workflowId,
config.failureRatePercent!,
config.windowHours!,
triggerFilter
)

case 'latency_threshold':
return checkLatencyThreshold(durationMs, config.durationThresholdMs!)
Expand All @@ -306,19 +309,24 @@ export async function shouldTriggerAlert(
workflowId,
durationMs,
config.latencySpikePercent!,
config.windowHours!
config.windowHours!,
triggerFilter
)

case 'cost_threshold':
return checkCostThreshold(cost, config.costThresholdDollars!)

case 'no_activity':
// no_activity alerts are handled by the hourly polling job, not execution events
return false

case 'error_count':
if (status !== 'error') return false
return checkErrorCount(workflowId, config.errorCountThreshold!, config.windowHours!)
return checkErrorCount(
workflowId,
config.errorCountThreshold!,
config.windowHours!,
triggerFilter
)

default:
logger.warn(`Unknown alert rule: ${rule}`)
Expand Down
Loading
Loading