diff --git a/apps/sim/app/api/emails/preview/route.ts b/apps/sim/app/api/emails/preview/route.ts index 5047a3eaa6..80c298bdad 100644 --- a/apps/sim/app/api/emails/preview/route.ts +++ b/apps/sim/app/api/emails/preview/route.ts @@ -15,6 +15,7 @@ import { renderPlanWelcomeEmail, renderUsageThresholdEmail, renderWelcomeEmail, + renderWorkflowNotificationEmail, renderWorkspaceInvitationEmail, } from '@/components/emails' @@ -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 @@ -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) diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts index 0fff019545..5637ad1f4c 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts @@ -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(), @@ -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, @@ -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 @@ -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, diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts index 3e95e22205..ade0689ae0 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts @@ -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' @@ -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 } @@ -157,23 +157,26 @@ async function testEmail(subscription: typeof workspaceNotificationSubscription. const { payload } = buildTestPayload(subscription) const data = (payload as Record).data as Record + 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)?.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: ` -
-

Test Notification

-

This is a test notification from Sim Studio.

- - - - -
Workflow${data.workflowName}
Status${data.status}
Duration${data.totalDurationMs}ms
-

This notification is configured for workspace notifications.

-
- `, + 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', }) @@ -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.', }, ], }, diff --git a/apps/sim/app/api/workspaces/[id]/notifications/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/route.ts index b5852a0182..ef630045cd 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/route.ts @@ -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(), @@ -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, @@ -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, @@ -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, diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx index 1d78fe2db6..684b0394f3 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx @@ -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: '', @@ -203,7 +202,6 @@ export function NotificationSettings({ levelFilter: ['info', 'error'], triggerFilter: [...CORE_TRIGGER_TYPES], includeFinalOutput: false, - includeTraceSpans: false, includeRateLimits: false, includeUsageData: false, webhookUrl: '', @@ -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, @@ -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 || '', @@ -830,7 +826,6 @@ export function NotificationSettings({ { const labels: Record = { 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[] diff --git a/apps/sim/background/workspace-notification-delivery.ts b/apps/sim/background/workspace-notification-delivery.ts index e0356f91d8..8c0c350731 100644 --- a/apps/sim/background/workspace-notification-delivery.ts +++ b/apps/sim/background/workspace-notification-delivery.ts @@ -10,12 +10,17 @@ import { createLogger } from '@sim/logger' import { task } from '@trigger.dev/sdk' import { and, eq, isNull, lte, or, sql } from 'drizzle-orm' import { v4 as uuidv4 } from 'uuid' +import { + type EmailRateLimitsData, + type EmailUsageData, + renderWorkflowNotificationEmail, +} from '@/components/emails' import { checkUsageStatus } from '@/lib/billing/calculations/usage-monitor' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { RateLimiter } from '@/lib/core/rate-limiter' import { decryptSecret } from '@/lib/core/security/encryption' import { getBaseUrl } from '@/lib/core/utils/urls' -import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' +import type { WorkflowExecutionLog } from '@/lib/logs/types' import { sendEmail } from '@/lib/messaging/email/mailer' import type { AlertConfig } from '@/lib/notifications/alert-rules' @@ -45,9 +50,8 @@ interface NotificationPayload { totalDurationMs: number cost?: Record finalOutput?: unknown - traceSpans?: unknown[] - rateLimits?: Record - usage?: Record + rateLimits?: EmailRateLimitsData + usage?: EmailUsageData } } @@ -94,10 +98,6 @@ async function buildPayload( payload.data.finalOutput = executionData.finalOutput } - if (subscription.includeTraceSpans && executionData.traceSpans) { - payload.data.traceSpans = executionData.traceSpans as unknown[] - } - if (subscription.includeRateLimits && userId) { try { const userSubscription = await getHighestPrioritySubscription(userId) @@ -251,18 +251,6 @@ function formatAlertReason(alertConfig: AlertConfig): string { } } -function formatJsonForEmail(data: unknown, label: string): string { - if (!data) return '' - const json = JSON.stringify(data, null, 2) - const escapedJson = json.replace(//g, '>') - return ` -
-

${label}

-
${escapedJson}
-
- ` -} - async function deliverEmail( subscription: typeof workspaceNotificationSubscription.$inferSelect, payload: NotificationPayload, @@ -275,8 +263,7 @@ async function deliverEmail( const isError = payload.data.status !== 'success' const statusText = isError ? 'Error' : 'Success' const logUrl = buildLogUrl(subscription.workspaceId, payload.data.executionId) - const baseUrl = getBaseUrl() - const alertReason = alertConfig ? formatAlertReason(alertConfig) : null + const alertReason = alertConfig ? formatAlertReason(alertConfig) : undefined // Build subject line const subject = alertReason @@ -285,113 +272,36 @@ async function deliverEmail( ? `Error Alert: ${payload.data.workflowName}` : `Workflow Completed: ${payload.data.workflowName}` - let includedDataHtml = '' + // Build plain text for fallback let includedDataText = '' - if (payload.data.finalOutput) { - includedDataHtml += formatJsonForEmail(payload.data.finalOutput, 'Final Output') includedDataText += `\n\nFinal Output:\n${JSON.stringify(payload.data.finalOutput, null, 2)}` } - - if ( - payload.data.traceSpans && - Array.isArray(payload.data.traceSpans) && - payload.data.traceSpans.length > 0 - ) { - includedDataHtml += formatJsonForEmail(payload.data.traceSpans, 'Trace Spans') - includedDataText += `\n\nTrace Spans:\n${JSON.stringify(payload.data.traceSpans, null, 2)}` - } - if (payload.data.rateLimits) { - includedDataHtml += formatJsonForEmail(payload.data.rateLimits, 'Rate Limits') includedDataText += `\n\nRate Limits:\n${JSON.stringify(payload.data.rateLimits, null, 2)}` } - if (payload.data.usage) { - includedDataHtml += formatJsonForEmail(payload.data.usage, 'Usage Data') includedDataText += `\n\nUsage Data:\n${JSON.stringify(payload.data.usage, null, 2)}` } + // Render the email using the shared template + const html = await renderWorkflowNotificationEmail({ + workflowName: payload.data.workflowName || 'Unknown Workflow', + status: payload.data.status, + trigger: payload.data.trigger, + duration: formatDuration(payload.data.totalDurationMs), + cost: formatCost(payload.data.cost), + logUrl, + alertReason, + finalOutput: payload.data.finalOutput, + rateLimits: payload.data.rateLimits, + usageData: payload.data.usage, + }) + const result = await sendEmail({ to: subscription.emailRecipients, subject, - html: ` - - - - - - - -
- -
- Sim Studio -
- - -
-
-
-
-
- - -
-

- ${alertReason ? 'Alert Triggered' : isError ? 'Workflow Execution Failed' : 'Workflow Execution Completed'} -

- ${alertReason ? `

Reason: ${alertReason}

` : ''} - - - - - - - - - - - - - - - - - - - - - - -
Workflow${payload.data.workflowName}
Status${statusText}
Trigger${payload.data.trigger}
Duration${formatDuration(payload.data.totalDurationMs)}
Cost${formatCost(payload.data.cost)}
- - - View Execution Log → - - - ${includedDataHtml} - -

- Best regards,
- The Sim Team -

-
-
- - -
-

- © ${new Date().getFullYear()} Sim Studio, All Rights Reserved -

-

- Privacy Policy • - Terms of Service -

-
- - - `, + html, text: `${subject}\n${alertReason ? `\nReason: ${alertReason}\n` : ''}\nWorkflow: ${payload.data.workflowName}\nStatus: ${statusText}\nTrigger: ${payload.data.trigger}\nDuration: ${formatDuration(payload.data.totalDurationMs)}\nCost: ${formatCost(payload.data.cost)}\n\nView Log: ${logUrl}${includedDataText}`, emailType: 'notifications', }) @@ -479,26 +389,6 @@ async function deliverSlack( }) } - if ( - payload.data.traceSpans && - Array.isArray(payload.data.traceSpans) && - payload.data.traceSpans.length > 0 - ) { - const spansSummary = (payload.data.traceSpans as TraceSpan[]) - .map((span) => { - const status = span.status === 'success' ? '✓' : '✗' - return `${status} ${span.name || 'Unknown'} (${formatDuration(span.duration || 0)})` - }) - .join('\n') - blocks.push({ - type: 'section', - text: { - type: 'mrkdwn', - text: `*Trace Spans:*\n\`\`\`${spansSummary}\`\`\``, - }, - }) - } - if (payload.data.rateLimits) { const limitsStr = JSON.stringify(payload.data.rateLimits, null, 2) blocks.push({ diff --git a/apps/sim/components/emails/_styles/base.ts b/apps/sim/components/emails/_styles/base.ts index 844ac1c55f..4b9e2f9e6b 100644 --- a/apps/sim/components/emails/_styles/base.ts +++ b/apps/sim/components/emails/_styles/base.ts @@ -173,6 +173,17 @@ export const baseStyles = { margin: 0, }, + /** Code block text (for JSON/code display) */ + codeBlock: { + fontSize: typography.fontSize.caption, + lineHeight: typography.lineHeight.caption, + color: colors.textSecondary, + fontFamily: 'monospace', + whiteSpace: 'pre-wrap' as const, + wordWrap: 'break-word' as const, + margin: 0, + }, + /** Highlighted info box (e.g., "What you get with Pro") */ infoBox: { backgroundColor: colors.bgOuter, diff --git a/apps/sim/components/emails/index.ts b/apps/sim/components/emails/index.ts index 6cd1fea0db..419b250b95 100644 --- a/apps/sim/components/emails/index.ts +++ b/apps/sim/components/emails/index.ts @@ -10,6 +10,8 @@ export * from './careers' export * from './components' // Invitation emails export * from './invitations' +// Notification emails +export * from './notifications' // Render functions and subjects export * from './render' export * from './subjects' diff --git a/apps/sim/components/emails/notifications/index.ts b/apps/sim/components/emails/notifications/index.ts new file mode 100644 index 0000000000..52de16601f --- /dev/null +++ b/apps/sim/components/emails/notifications/index.ts @@ -0,0 +1,7 @@ +export type { + EmailRateLimitStatus, + EmailRateLimitsData, + EmailUsageData, + WorkflowNotificationEmailProps, +} from './workflow-notification-email' +export { WorkflowNotificationEmail } from './workflow-notification-email' diff --git a/apps/sim/components/emails/notifications/workflow-notification-email.tsx b/apps/sim/components/emails/notifications/workflow-notification-email.tsx new file mode 100644 index 0000000000..88ad6fba66 --- /dev/null +++ b/apps/sim/components/emails/notifications/workflow-notification-email.tsx @@ -0,0 +1,161 @@ +import { Link, Section, Text } from '@react-email/components' +import { baseStyles } from '@/components/emails/_styles' +import { EmailLayout } from '@/components/emails/components' +import { getBrandConfig } from '@/lib/branding/branding' + +/** + * Serialized rate limit status for email payloads. + * Note: This differs from the canonical RateLimitStatus in @/lib/core/rate-limiter + * which uses Date for resetAt. This version uses string for JSON serialization. + */ +export interface EmailRateLimitStatus { + requestsPerMinute: number + remaining: number + maxBurst?: number + resetAt?: string +} + +export interface EmailRateLimitsData { + sync?: EmailRateLimitStatus + async?: EmailRateLimitStatus +} + +export interface EmailUsageData { + currentPeriodCost: number + limit: number + percentUsed: number + isExceeded?: boolean +} + +export interface WorkflowNotificationEmailProps { + workflowName: string + status: 'success' | 'error' + trigger: string + duration: string + cost: string + logUrl: string + alertReason?: string + finalOutput?: unknown + rateLimits?: EmailRateLimitsData + usageData?: EmailUsageData +} + +function formatJsonForEmail(data: unknown): string { + return JSON.stringify(data, null, 2) +} + +export function WorkflowNotificationEmail({ + workflowName, + status, + trigger, + duration, + cost, + logUrl, + alertReason, + finalOutput, + rateLimits, + usageData, +}: WorkflowNotificationEmailProps) { + const brand = getBrandConfig() + const isError = status === 'error' + const statusText = isError ? 'Error' : 'Success' + + const previewText = alertReason + ? `${brand.name}: Alert - ${workflowName}` + : isError + ? `${brand.name}: Workflow Failed - ${workflowName}` + : `${brand.name}: Workflow Completed - ${workflowName}` + + const message = alertReason + ? 'An alert was triggered for your workflow.' + : isError + ? 'Your workflow execution failed.' + : 'Your workflow completed successfully.' + + return ( + + Hello, + {message} + +
+ {alertReason && ( + + Reason: {alertReason} + + )} + + Workflow: {workflowName} + + + Status: {statusText} + + + Trigger: {trigger} + + + Duration: {duration} + + + Cost: {cost} + +
+ + + View Execution Log + + + {rateLimits && (rateLimits.sync || rateLimits.async) ? ( + <> +
+
+ Rate Limits + {rateLimits.sync && ( + + Sync: {rateLimits.sync.remaining} of {rateLimits.sync.requestsPerMinute} remaining + + )} + {rateLimits.async && ( + + Async: {rateLimits.async.remaining} of {rateLimits.async.requestsPerMinute}{' '} + remaining + + )} +
+ + ) : null} + + {usageData ? ( + <> +
+
+ Usage + + ${usageData.currentPeriodCost.toFixed(2)} of ${usageData.limit.toFixed(2)} used ( + {usageData.percentUsed.toFixed(1)}%) + +
+ + ) : null} + + {finalOutput ? ( + <> +
+
+ Final Output + + {formatJsonForEmail(finalOutput)} + +
+ + ) : null} + +
+ + + You're receiving this because you subscribed to workflow notifications. + + + ) +} + +export default WorkflowNotificationEmail diff --git a/apps/sim/components/emails/render.ts b/apps/sim/components/emails/render.ts index 90522246aa..3c1395d051 100644 --- a/apps/sim/components/emails/render.ts +++ b/apps/sim/components/emails/render.ts @@ -15,6 +15,10 @@ import { PollingGroupInvitationEmail, WorkspaceInvitationEmail, } from '@/components/emails/invitations' +import { + WorkflowNotificationEmail, + type WorkflowNotificationEmailProps, +} from '@/components/emails/notifications' import { HelpConfirmationEmail } from '@/components/emails/support' import { getBaseUrl } from '@/lib/core/utils/urls' @@ -258,3 +262,9 @@ export async function renderCareersSubmissionEmail(params: { }) ) } + +export async function renderWorkflowNotificationEmail( + params: WorkflowNotificationEmailProps +): Promise { + return await render(WorkflowNotificationEmail(params)) +} diff --git a/apps/sim/hooks/queries/notifications.ts b/apps/sim/hooks/queries/notifications.ts index 49af2ed8d5..b92d77b1e9 100644 --- a/apps/sim/hooks/queries/notifications.ts +++ b/apps/sim/hooks/queries/notifications.ts @@ -61,7 +61,6 @@ export interface NotificationSubscription { levelFilter: LogLevel[] triggerFilter: TriggerType[] includeFinalOutput: boolean - includeTraceSpans: boolean includeRateLimits: boolean includeUsageData: boolean webhookConfig?: WebhookConfig | null @@ -106,7 +105,6 @@ interface CreateNotificationParams { levelFilter: LogLevel[] triggerFilter: TriggerType[] includeFinalOutput: boolean - includeTraceSpans: boolean includeRateLimits: boolean includeUsageData: boolean alertConfig?: AlertConfig | null diff --git a/apps/sim/lib/logs/events.ts b/apps/sim/lib/logs/events.ts index 767c4bd8a0..7ad832ad65 100644 --- a/apps/sim/lib/logs/events.ts +++ b/apps/sim/lib/logs/events.ts @@ -25,7 +25,6 @@ function prepareLogData( log: WorkflowExecutionLog, subscription: { includeFinalOutput: boolean - includeTraceSpans: boolean } ) { const preparedLog = { ...log, executionData: {} as Record } @@ -38,10 +37,6 @@ function prepareLogData( webhookData.finalOutput = data.finalOutput } - if (subscription.includeTraceSpans && data.traceSpans) { - webhookData.traceSpans = data.traceSpans - } - preparedLog.executionData = webhookData }