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
52 changes: 52 additions & 0 deletions apps/sim/app/api/emails/preview/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
renderPlanWelcomeEmail,
renderUsageThresholdEmail,
renderWelcomeEmail,
renderWorkflowNotificationEmail,
renderWorkspaceInvitationEmail,
} from '@/components/emails'

Expand Down Expand Up @@ -108,6 +109,51 @@ const emailTemplates = {
message:
'I have 10 years of experience building scalable distributed systems. Most recently, I led a team at a Series B startup where we scaled from 100K to 10M users.',
}),

// Notification emails
'workflow-notification-success': () =>
renderWorkflowNotificationEmail({
workflowName: 'Customer Onboarding Flow',
status: 'success',
trigger: 'api',
duration: '2.3s',
cost: '$0.0042',
logUrl: 'https://sim.ai/workspace/ws_123/logs?search=exec_abc123',
}),
'workflow-notification-error': () =>
renderWorkflowNotificationEmail({
workflowName: 'Customer Onboarding Flow',
status: 'error',
trigger: 'webhook',
duration: '1.1s',
cost: '$0.0021',
logUrl: 'https://sim.ai/workspace/ws_123/logs?search=exec_abc123',
}),
'workflow-notification-alert': () =>
renderWorkflowNotificationEmail({
workflowName: 'Customer Onboarding Flow',
status: 'error',
trigger: 'schedule',
duration: '45.2s',
cost: '$0.0156',
logUrl: 'https://sim.ai/workspace/ws_123/logs?search=exec_abc123',
alertReason: '3 consecutive failures detected',
}),
'workflow-notification-full': () =>
renderWorkflowNotificationEmail({
workflowName: 'Data Processing Pipeline',
status: 'success',
trigger: 'api',
duration: '12.5s',
cost: '$0.0234',
logUrl: 'https://sim.ai/workspace/ws_123/logs?search=exec_abc123',
finalOutput: { processed: 150, skipped: 3, status: 'completed' },
rateLimits: {
sync: { requestsPerMinute: 60, remaining: 45 },
async: { requestsPerMinute: 120, remaining: 98 },
},
usageData: { currentPeriodCost: 12.45, limit: 50, percentUsed: 24.9 },
}),
} as const

type EmailTemplate = keyof typeof emailTemplates
Expand All @@ -131,6 +177,12 @@ export async function GET(request: NextRequest) {
'payment-failed',
],
Careers: ['careers-confirmation', 'careers-submission'],
Notifications: [
'workflow-notification-success',
'workflow-notification-error',
'workflow-notification-alert',
'workflow-notification-full',
],
}

const categoryHtml = Object.entries(categories)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ const updateNotificationSchema = z
levelFilter: levelFilterSchema.optional(),
triggerFilter: triggerFilterSchema.optional(),
includeFinalOutput: z.boolean().optional(),
includeTraceSpans: z.boolean().optional(),
includeRateLimits: z.boolean().optional(),
includeUsageData: z.boolean().optional(),
alertConfig: alertConfigSchema.optional(),
Expand Down Expand Up @@ -147,7 +146,6 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
levelFilter: subscription.levelFilter,
triggerFilter: subscription.triggerFilter,
includeFinalOutput: subscription.includeFinalOutput,
includeTraceSpans: subscription.includeTraceSpans,
includeRateLimits: subscription.includeRateLimits,
includeUsageData: subscription.includeUsageData,
webhookConfig: subscription.webhookConfig,
Expand Down Expand Up @@ -222,7 +220,6 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
if (data.triggerFilter !== undefined) updateData.triggerFilter = data.triggerFilter
if (data.includeFinalOutput !== undefined)
updateData.includeFinalOutput = data.includeFinalOutput
if (data.includeTraceSpans !== undefined) updateData.includeTraceSpans = data.includeTraceSpans
if (data.includeRateLimits !== undefined) updateData.includeRateLimits = data.includeRateLimits
if (data.includeUsageData !== undefined) updateData.includeUsageData = data.includeUsageData
if (data.alertConfig !== undefined) updateData.alertConfig = data.alertConfig
Expand Down Expand Up @@ -260,7 +257,6 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
levelFilter: subscription.levelFilter,
triggerFilter: subscription.triggerFilter,
includeFinalOutput: subscription.includeFinalOutput,
includeTraceSpans: subscription.includeTraceSpans,
includeRateLimits: subscription.includeRateLimits,
includeUsageData: subscription.includeUsageData,
webhookConfig: subscription.webhookConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import {
type EmailRateLimitsData,
type EmailUsageData,
renderWorkflowNotificationEmail,
} from '@/components/emails'
import { getSession } from '@/lib/auth'
import { decryptSecret } from '@/lib/core/security/encryption'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

Expand Down Expand Up @@ -67,29 +73,23 @@ function buildTestPayload(subscription: typeof workspaceNotificationSubscription
data.finalOutput = { message: 'This is a test notification', test: true }
}

if (subscription.includeTraceSpans) {
data.traceSpans = [
{
id: 'span_test_1',
name: 'Test Block',
type: 'block',
status: 'success',
startTime: new Date(timestamp - 5000).toISOString(),
endTime: new Date(timestamp).toISOString(),
duration: 5000,
},
]
}

if (subscription.includeRateLimits) {
data.rateLimits = {
sync: { limit: 150, remaining: 45, resetAt: new Date(timestamp + 60000).toISOString() },
async: { limit: 1000, remaining: 50, resetAt: new Date(timestamp + 60000).toISOString() },
sync: {
requestsPerMinute: 150,
remaining: 45,
resetAt: new Date(timestamp + 60000).toISOString(),
},
async: {
requestsPerMinute: 1000,
remaining: 50,
resetAt: new Date(timestamp + 60000).toISOString(),
},
}
}

if (subscription.includeUsageData) {
data.usage = { currentPeriodCost: 2.45, limit: 20, plan: 'pro', isExceeded: false }
data.usage = { currentPeriodCost: 2.45, limit: 20, percentUsed: 12.25, isExceeded: false }
}

return { payload, timestamp }
Expand Down Expand Up @@ -157,23 +157,26 @@ async function testEmail(subscription: typeof workspaceNotificationSubscription.

const { payload } = buildTestPayload(subscription)
const data = (payload as Record<string, unknown>).data as Record<string, unknown>
const baseUrl = getBaseUrl()
const logUrl = `${baseUrl}/workspace/${subscription.workspaceId}/logs`

const html = await renderWorkflowNotificationEmail({
workflowName: data.workflowName as string,
status: data.status as 'success' | 'error',
trigger: data.trigger as string,
duration: `${data.totalDurationMs}ms`,
cost: `$${(((data.cost as Record<string, unknown>)?.total as number) || 0).toFixed(4)}`,
logUrl,
finalOutput: data.finalOutput,
rateLimits: data.rateLimits as EmailRateLimitsData | undefined,
usageData: data.usage as EmailUsageData | undefined,
})

const result = await sendEmail({
to: subscription.emailRecipients,
subject: `[Test] Workflow Execution: ${data.workflowName}`,
text: `This is a test notification from Sim Studio.\n\nWorkflow: ${data.workflowName}\nStatus: ${data.status}\nDuration: ${data.totalDurationMs}ms\n\nThis notification is configured for workspace notifications.`,
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #7F2FFF;">Test Notification</h2>
<p>This is a test notification from Sim Studio.</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr><td style="padding: 8px; border: 1px solid #eee;"><strong>Workflow</strong></td><td style="padding: 8px; border: 1px solid #eee;">${data.workflowName}</td></tr>
<tr><td style="padding: 8px; border: 1px solid #eee;"><strong>Status</strong></td><td style="padding: 8px; border: 1px solid #eee;">${data.status}</td></tr>
<tr><td style="padding: 8px; border: 1px solid #eee;"><strong>Duration</strong></td><td style="padding: 8px; border: 1px solid #eee;">${data.totalDurationMs}ms</td></tr>
</table>
<p style="color: #666; font-size: 12px;">This notification is configured for workspace notifications.</p>
</div>
`,
html,
text: `This is a test notification from Sim.\n\nWorkflow: ${data.workflowName}\nStatus: ${data.status}\nDuration: ${data.totalDurationMs}ms\n\nView Log: ${logUrl}\n\nThis notification is configured for workspace notifications.`,
emailType: 'notifications',
})

Expand Down Expand Up @@ -227,7 +230,7 @@ async function testSlack(
elements: [
{
type: 'mrkdwn',
text: 'This is a test notification from Sim Studio workspace notifications.',
text: 'This is a test notification from Sim workspace notifications.',
},
],
},
Expand Down
4 changes: 0 additions & 4 deletions apps/sim/app/api/workspaces/[id]/notifications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ const createNotificationSchema = z
levelFilter: levelFilterSchema.default(['info', 'error']),
triggerFilter: triggerFilterSchema.default([...CORE_TRIGGER_TYPES]),
includeFinalOutput: z.boolean().default(false),
includeTraceSpans: z.boolean().default(false),
includeRateLimits: z.boolean().default(false),
includeUsageData: z.boolean().default(false),
alertConfig: alertConfigSchema.optional(),
Expand Down Expand Up @@ -138,7 +137,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
levelFilter: workspaceNotificationSubscription.levelFilter,
triggerFilter: workspaceNotificationSubscription.triggerFilter,
includeFinalOutput: workspaceNotificationSubscription.includeFinalOutput,
includeTraceSpans: workspaceNotificationSubscription.includeTraceSpans,
includeRateLimits: workspaceNotificationSubscription.includeRateLimits,
includeUsageData: workspaceNotificationSubscription.includeUsageData,
webhookConfig: workspaceNotificationSubscription.webhookConfig,
Expand Down Expand Up @@ -240,7 +238,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
levelFilter: data.levelFilter,
triggerFilter: data.triggerFilter,
includeFinalOutput: data.includeFinalOutput,
includeTraceSpans: data.includeTraceSpans,
includeRateLimits: data.includeRateLimits,
includeUsageData: data.includeUsageData,
alertConfig: data.alertConfig || null,
Expand All @@ -266,7 +263,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
levelFilter: subscription.levelFilter,
triggerFilter: subscription.triggerFilter,
includeFinalOutput: subscription.includeFinalOutput,
includeTraceSpans: subscription.includeTraceSpans,
includeRateLimits: subscription.includeRateLimits,
includeUsageData: subscription.includeUsageData,
webhookConfig: subscription.webhookConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ export function NotificationSettings({
levelFilter: ['info', 'error'] as LogLevel[],
triggerFilter: [...CORE_TRIGGER_TYPES] as CoreTriggerType[],
includeFinalOutput: false,
includeTraceSpans: false,
includeRateLimits: false,
includeUsageData: false,
webhookUrl: '',
Expand Down Expand Up @@ -203,7 +202,6 @@ export function NotificationSettings({
levelFilter: ['info', 'error'],
triggerFilter: [...CORE_TRIGGER_TYPES],
includeFinalOutput: false,
includeTraceSpans: false,
includeRateLimits: false,
includeUsageData: false,
webhookUrl: '',
Expand Down Expand Up @@ -422,7 +420,6 @@ export function NotificationSettings({
levelFilter: formData.levelFilter,
triggerFilter: formData.triggerFilter,
includeFinalOutput: formData.includeFinalOutput,
includeTraceSpans: formData.includeTraceSpans,
includeRateLimits: formData.includeRateLimits,
includeUsageData: formData.includeUsageData,
alertConfig,
Expand Down Expand Up @@ -474,7 +471,6 @@ export function NotificationSettings({
levelFilter: subscription.levelFilter as LogLevel[],
triggerFilter: subscription.triggerFilter as CoreTriggerType[],
includeFinalOutput: subscription.includeFinalOutput,
includeTraceSpans: subscription.includeTraceSpans,
includeRateLimits: subscription.includeRateLimits,
includeUsageData: subscription.includeUsageData,
webhookUrl: subscription.webhookConfig?.url || '',
Expand Down Expand Up @@ -830,15 +826,13 @@ export function NotificationSettings({
<Combobox
options={[
{ label: 'Final Output', value: 'includeFinalOutput' },
{ label: 'Trace Spans', value: 'includeTraceSpans' },
{ label: 'Rate Limits', value: 'includeRateLimits' },
{ label: 'Usage Data', value: 'includeUsageData' },
]}
multiSelect
multiSelectValues={
[
formData.includeFinalOutput && 'includeFinalOutput',
formData.includeTraceSpans && 'includeTraceSpans',
formData.includeRateLimits && 'includeRateLimits',
formData.includeUsageData && 'includeUsageData',
].filter(Boolean) as string[]
Expand All @@ -847,7 +841,6 @@ export function NotificationSettings({
setFormData({
...formData,
includeFinalOutput: values.includes('includeFinalOutput'),
includeTraceSpans: values.includes('includeTraceSpans'),
includeRateLimits: values.includes('includeRateLimits'),
includeUsageData: values.includes('includeUsageData'),
})
Expand All @@ -856,13 +849,11 @@ export function NotificationSettings({
overlayContent={(() => {
const labels: Record<string, string> = {
includeFinalOutput: 'Final Output',
includeTraceSpans: 'Trace Spans',
includeRateLimits: 'Rate Limits',
includeUsageData: 'Usage Data',
}
const selected = [
formData.includeFinalOutput && 'includeFinalOutput',
formData.includeTraceSpans && 'includeTraceSpans',
formData.includeRateLimits && 'includeRateLimits',
formData.includeUsageData && 'includeUsageData',
].filter(Boolean) as string[]
Expand Down
Loading