mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
feat(deployment-version): capture deployment version in log (#2304)
* feat(deployment-version): capture deployment version in log * improvement: terminal store, logs version, toolbar --------- Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
|
||||
import {
|
||||
permissions,
|
||||
workflow,
|
||||
workflowDeploymentVersion,
|
||||
workflowExecutionLogs,
|
||||
} from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -29,6 +34,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
executionId: workflowExecutionLogs.executionId,
|
||||
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
|
||||
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
|
||||
level: workflowExecutionLogs.level,
|
||||
trigger: workflowExecutionLogs.trigger,
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
@@ -46,9 +52,15 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
||||
workflowWorkspaceId: workflow.workspaceId,
|
||||
workflowCreatedAt: workflow.createdAt,
|
||||
workflowUpdatedAt: workflow.updatedAt,
|
||||
deploymentVersion: workflowDeploymentVersion.version,
|
||||
deploymentVersionName: workflowDeploymentVersion.name,
|
||||
})
|
||||
.from(workflowExecutionLogs)
|
||||
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||
.leftJoin(
|
||||
workflowDeploymentVersion,
|
||||
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
|
||||
)
|
||||
.innerJoin(
|
||||
permissions,
|
||||
and(
|
||||
@@ -81,6 +93,9 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
||||
id: log.id,
|
||||
workflowId: log.workflowId,
|
||||
executionId: log.executionId,
|
||||
deploymentVersionId: log.deploymentVersionId,
|
||||
deploymentVersion: log.deploymentVersion ?? null,
|
||||
deploymentVersionName: log.deploymentVersionName ?? null,
|
||||
level: log.level,
|
||||
duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null,
|
||||
trigger: log.trigger,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { db } from '@sim/db'
|
||||
import { pausedExecutions, permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
|
||||
import {
|
||||
pausedExecutions,
|
||||
permissions,
|
||||
workflow,
|
||||
workflowDeploymentVersion,
|
||||
workflowExecutionLogs,
|
||||
} from '@sim/db/schema'
|
||||
import { and, desc, eq, gte, inArray, isNotNull, isNull, lte, or, type SQL, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
@@ -51,6 +57,7 @@ export async function GET(request: NextRequest) {
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
executionId: workflowExecutionLogs.executionId,
|
||||
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
|
||||
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
|
||||
level: workflowExecutionLogs.level,
|
||||
trigger: workflowExecutionLogs.trigger,
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
@@ -71,6 +78,8 @@ export async function GET(request: NextRequest) {
|
||||
pausedStatus: pausedExecutions.status,
|
||||
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
|
||||
pausedResumedCount: pausedExecutions.resumedCount,
|
||||
deploymentVersion: workflowDeploymentVersion.version,
|
||||
deploymentVersionName: workflowDeploymentVersion.name,
|
||||
}
|
||||
: {
|
||||
// Basic mode - exclude large fields for better performance
|
||||
@@ -78,6 +87,7 @@ export async function GET(request: NextRequest) {
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
executionId: workflowExecutionLogs.executionId,
|
||||
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
|
||||
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
|
||||
level: workflowExecutionLogs.level,
|
||||
trigger: workflowExecutionLogs.trigger,
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
@@ -98,6 +108,8 @@ export async function GET(request: NextRequest) {
|
||||
pausedStatus: pausedExecutions.status,
|
||||
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
|
||||
pausedResumedCount: pausedExecutions.resumedCount,
|
||||
deploymentVersion: workflowDeploymentVersion.version,
|
||||
deploymentVersionName: sql<null>`NULL`, // Only needed in full mode for details panel
|
||||
}
|
||||
|
||||
const baseQuery = db
|
||||
@@ -107,6 +119,10 @@ export async function GET(request: NextRequest) {
|
||||
pausedExecutions,
|
||||
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
|
||||
)
|
||||
.leftJoin(
|
||||
workflowDeploymentVersion,
|
||||
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
|
||||
)
|
||||
.innerJoin(
|
||||
workflow,
|
||||
and(
|
||||
@@ -397,6 +413,9 @@ export async function GET(request: NextRequest) {
|
||||
id: log.id,
|
||||
workflowId: log.workflowId,
|
||||
executionId: log.executionId,
|
||||
deploymentVersionId: log.deploymentVersionId,
|
||||
deploymentVersion: log.deploymentVersion ?? null,
|
||||
deploymentVersionName: log.deploymentVersionName ?? null,
|
||||
level: log.level,
|
||||
duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null,
|
||||
trigger: log.trigger,
|
||||
|
||||
@@ -111,6 +111,7 @@ export async function GET(request: NextRequest) {
|
||||
id: workflowExecutionLogs.id,
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
executionId: workflowExecutionLogs.executionId,
|
||||
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
|
||||
level: workflowExecutionLogs.level,
|
||||
trigger: workflowExecutionLogs.trigger,
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
@@ -161,6 +162,7 @@ export async function GET(request: NextRequest) {
|
||||
id: log.id,
|
||||
workflowId: log.workflowId,
|
||||
executionId: log.executionId,
|
||||
deploymentVersionId: log.deploymentVersionId,
|
||||
level: log.level,
|
||||
trigger: log.trigger,
|
||||
startedAt: log.startedAt.toISOString(),
|
||||
|
||||
@@ -430,6 +430,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
edges: any[]
|
||||
loops: Record<string, any>
|
||||
parallels: Record<string, any>
|
||||
deploymentVersionId?: string
|
||||
} | null = null
|
||||
|
||||
let processedInput = input
|
||||
@@ -444,6 +445,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
edges: workflowData.edges,
|
||||
loops: workflowData.loops || {},
|
||||
parallels: workflowData.parallels || {},
|
||||
deploymentVersionId:
|
||||
!shouldUseDraftState && 'deploymentVersionId' in workflowData
|
||||
? (workflowData.deploymentVersionId as string)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
const serializedWorkflow = new Serializer().serializeWorkflow(
|
||||
|
||||
@@ -199,7 +199,7 @@ export function Knowledge() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-[24px] grid grid-cols-1 gap-x-[20px] gap-y-[40px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
|
||||
<div className='mt-[24px] grid grid-cols-1 gap-[20px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
|
||||
{isLoading ? (
|
||||
<BaseCardSkeletonGrid count={8} />
|
||||
) : filteredAndSortedKnowledgeBases.length === 0 ? (
|
||||
|
||||
@@ -30,8 +30,8 @@ export function LineChart({
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const uniqueId = useRef(`chart-${Math.random().toString(36).substring(2, 9)}`).current
|
||||
const [containerWidth, setContainerWidth] = useState<number>(420)
|
||||
const width = containerWidth
|
||||
const [containerWidth, setContainerWidth] = useState<number | null>(null)
|
||||
const width = containerWidth ?? 0
|
||||
const height = 166
|
||||
const padding = { top: 16, right: 28, bottom: 26, left: 26 }
|
||||
useEffect(() => {
|
||||
@@ -39,14 +39,14 @@ export function LineChart({
|
||||
const element = containerRef.current
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
if (entry?.contentRect) {
|
||||
if (entry?.contentRect && entry.contentRect.width > 0) {
|
||||
const w = Math.max(280, Math.floor(entry.contentRect.width))
|
||||
setContainerWidth(w)
|
||||
}
|
||||
})
|
||||
ro.observe(element)
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (rect?.width) setContainerWidth(Math.max(280, Math.floor(rect.width)))
|
||||
if (rect?.width && rect.width > 0) setContainerWidth(Math.max(280, Math.floor(rect.width)))
|
||||
return () => ro.disconnect()
|
||||
}, [])
|
||||
const chartWidth = width - padding.left - padding.right
|
||||
@@ -95,6 +95,16 @@ export function LineChart({
|
||||
|
||||
const hasExternalWrapper = !label || label === ''
|
||||
|
||||
if (containerWidth === null) {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('w-full', !hasExternalWrapper && 'rounded-lg border bg-card p-4')}
|
||||
style={{ height }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -40,6 +40,14 @@ interface WorkflowExecution {
|
||||
const DEFAULT_SEGMENTS = 72
|
||||
const MIN_SEGMENT_PX = 10
|
||||
|
||||
/**
|
||||
* Predetermined heights for skeleton bars to avoid hydration mismatch.
|
||||
* Using static values instead of Math.random() ensures server/client consistency.
|
||||
*/
|
||||
const SKELETON_BAR_HEIGHTS = [
|
||||
45, 72, 38, 85, 52, 68, 30, 90, 55, 42, 78, 35, 88, 48, 65, 28, 82, 58, 40, 75, 32, 95, 50, 70,
|
||||
]
|
||||
|
||||
/**
|
||||
* Skeleton loader for a single graph card
|
||||
*/
|
||||
@@ -56,12 +64,12 @@ function GraphCardSkeleton({ title }: { title: string }) {
|
||||
<div className='flex h-[166px] flex-col justify-end gap-[4px]'>
|
||||
{/* Skeleton bars simulating chart */}
|
||||
<div className='flex items-end gap-[2px]'>
|
||||
{Array.from({ length: 24 }).map((_, i) => (
|
||||
{SKELETON_BAR_HEIGHTS.map((height, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
className='flex-1'
|
||||
style={{
|
||||
height: `${Math.random() * 80 + 20}%`,
|
||||
height: `${height}%`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -803,7 +811,6 @@ export default function Dashboard({
|
||||
<div className='flex-1 overflow-y-auto rounded-t-[6px] bg-[var(--surface-1)] px-[14px] py-[10px]'>
|
||||
{globalDetails ? (
|
||||
<LineChart
|
||||
key={`runs-${expandedWorkflowId || 'all'}-${Object.keys(selectedSegments).length}-${filteredExecutions.length}`}
|
||||
data={globalDetails.executionCounts}
|
||||
label=''
|
||||
color='var(--brand-tertiary)'
|
||||
@@ -832,7 +839,6 @@ export default function Dashboard({
|
||||
<div className='flex-1 overflow-y-auto rounded-t-[6px] bg-[var(--surface-1)] px-[14px] py-[10px]'>
|
||||
{globalDetails ? (
|
||||
<LineChart
|
||||
key={`errors-${expandedWorkflowId || 'all'}-${Object.keys(selectedSegments).length}-${filteredExecutions.length}`}
|
||||
data={globalDetails.failureCounts}
|
||||
label=''
|
||||
color='var(--text-error)'
|
||||
@@ -861,7 +867,6 @@ export default function Dashboard({
|
||||
<div className='flex-1 overflow-y-auto rounded-t-[6px] bg-[var(--surface-1)] px-[14px] py-[10px]'>
|
||||
{globalDetails ? (
|
||||
<LineChart
|
||||
key={`latency-${expandedWorkflowId || 'all'}-${Object.keys(selectedSegments).length}-${filteredExecutions.length}`}
|
||||
data={globalDetails.latencies}
|
||||
label=''
|
||||
color='var(--c-F59E0B)'
|
||||
|
||||
@@ -209,7 +209,7 @@ export function LogDetails({
|
||||
)}
|
||||
|
||||
{/* Details Section */}
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex min-w-0 flex-col overflow-hidden'>
|
||||
{/* Level */}
|
||||
<div className='flex h-[48px] items-center justify-between border-[var(--border)] border-b p-[8px]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
@@ -233,14 +233,30 @@ export function LogDetails({
|
||||
</div>
|
||||
|
||||
{/* Duration */}
|
||||
<div className='flex h-[48px] items-center justify-between p-[8px]'>
|
||||
<div
|
||||
className={`flex h-[48px] items-center justify-between border-b p-[8px] ${log.deploymentVersion ? 'border-[var(--border)]' : 'border-transparent'}`}
|
||||
>
|
||||
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
Duration
|
||||
</span>
|
||||
<span className='font-medium text-[14px] text-[var(--text-secondary)]'>
|
||||
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
{log.duration || '—'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Version */}
|
||||
{log.deploymentVersion && (
|
||||
<div className='flex h-[48px] items-center gap-[8px] p-[8px]'>
|
||||
<span className='flex-shrink-0 font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
Version
|
||||
</span>
|
||||
<div className='flex w-0 flex-1 justify-end'>
|
||||
<span className='max-w-full truncate rounded-[6px] bg-[#14291B] px-[9px] py-[2px] font-medium text-[#86EFAC] text-[12px]'>
|
||||
{log.deploymentVersionName || `v${log.deploymentVersion}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Workflow State */}
|
||||
|
||||
@@ -343,17 +343,18 @@ export function LogsToolbar({
|
||||
</Button>
|
||||
|
||||
{/* View mode toggle */}
|
||||
<div className='flex h-[32px] items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-elevated)] p-[2px]'>
|
||||
<div
|
||||
className='flex h-[32px] cursor-pointer items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-elevated)] p-[2px]'
|
||||
onClick={() => onViewModeChange(isDashboardView ? 'logs' : 'dashboard')}
|
||||
>
|
||||
<Button
|
||||
variant={!isDashboardView ? 'active' : 'ghost'}
|
||||
onClick={() => onViewModeChange('logs')}
|
||||
className='h-[26px] rounded-[4px] px-[10px]'
|
||||
>
|
||||
Logs
|
||||
</Button>
|
||||
<Button
|
||||
variant={isDashboardView ? 'active' : 'ghost'}
|
||||
onClick={() => onViewModeChange('dashboard')}
|
||||
className='h-[26px] rounded-[4px] px-[10px]'
|
||||
>
|
||||
Dashboard
|
||||
|
||||
@@ -609,7 +609,7 @@ export default function Logs() {
|
||||
|
||||
{/* Log Details - rendered inside table container */}
|
||||
<LogDetails
|
||||
log={logDetailQuery.data || selectedLog}
|
||||
log={logDetailQuery.data ? { ...selectedLog, ...logDetailQuery.data } : selectedLog}
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={handleCloseSidebar}
|
||||
onNavigateNext={handleNavigateNext}
|
||||
|
||||
@@ -298,6 +298,8 @@ export function Terminal() {
|
||||
setOutputPanelWidth,
|
||||
openOnRun,
|
||||
setOpenOnRun,
|
||||
wrapText,
|
||||
setWrapText,
|
||||
setHasHydrated,
|
||||
} = useTerminalStore()
|
||||
const isExpanded = useTerminalStore((state) => state.terminalHeight > NEAR_MIN_THRESHOLD)
|
||||
@@ -312,7 +314,6 @@ export function Terminal() {
|
||||
const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV)
|
||||
const [selectedEntry, setSelectedEntry] = useState<ConsoleEntry | null>(null)
|
||||
const [isToggling, setIsToggling] = useState(false)
|
||||
const [wrapText, setWrapText] = useState(true)
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
||||
const [showInput, setShowInput] = useState(false)
|
||||
const [autoSelectEnabled, setAutoSelectEnabled] = useState(true)
|
||||
@@ -1528,7 +1529,7 @@ export function Terminal() {
|
||||
showCheck
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setWrapText((prev) => !prev)
|
||||
setWrapText(!wrapText)
|
||||
}}
|
||||
>
|
||||
<span>Wrap text</span>
|
||||
|
||||
@@ -193,6 +193,7 @@ async function runWorkflowExecution({
|
||||
const deployedData = await loadDeployedWorkflowState(payload.workflowId)
|
||||
|
||||
const blocks = deployedData.blocks
|
||||
const { deploymentVersionId } = deployedData
|
||||
logger.info(`[${requestId}] Loaded deployed workflow ${payload.workflowId}`)
|
||||
|
||||
if (payload.blockId) {
|
||||
@@ -233,6 +234,7 @@ async function runWorkflowExecution({
|
||||
userId: actorUserId,
|
||||
workspaceId: workflowRecord.workspaceId || '',
|
||||
variables: variables || {},
|
||||
deploymentVersionId,
|
||||
})
|
||||
|
||||
const metadata: ExecutionMetadata = {
|
||||
|
||||
@@ -138,18 +138,26 @@ async function executeWebhookJobInternal(
|
||||
requestId
|
||||
)
|
||||
|
||||
// Track deploymentVersionId at function scope so it's available in catch block
|
||||
let deploymentVersionId: string | undefined
|
||||
|
||||
try {
|
||||
const workflowData =
|
||||
payload.executionTarget === 'live'
|
||||
const useDraftState = payload.executionTarget === 'live'
|
||||
const workflowData = useDraftState
|
||||
? await loadWorkflowFromNormalizedTables(payload.workflowId)
|
||||
: await loadDeployedWorkflowState(payload.workflowId)
|
||||
if (!workflowData) {
|
||||
throw new Error(
|
||||
`Workflow state not found. The workflow may not be ${payload.executionTarget === 'live' ? 'saved' : 'deployed'} or the deployment data may be corrupted.`
|
||||
`Workflow state not found. The workflow may not be ${useDraftState ? 'saved' : 'deployed'} or the deployment data may be corrupted.`
|
||||
)
|
||||
}
|
||||
|
||||
const { blocks, edges, loops, parallels } = workflowData
|
||||
// Only deployed executions have a deployment version ID
|
||||
deploymentVersionId =
|
||||
!useDraftState && 'deploymentVersionId' in workflowData
|
||||
? (workflowData.deploymentVersionId as string)
|
||||
: undefined
|
||||
|
||||
const wfRows = await db
|
||||
.select({ workspaceId: workflowTable.workspaceId, variables: workflowTable.variables })
|
||||
@@ -229,6 +237,13 @@ async function executeWebhookJobInternal(
|
||||
useDraftState: false,
|
||||
startTime: new Date().toISOString(),
|
||||
isClientSession: false,
|
||||
workflowStateOverride: {
|
||||
blocks,
|
||||
edges,
|
||||
loops: loops || {},
|
||||
parallels: parallels || {},
|
||||
deploymentVersionId,
|
||||
},
|
||||
}
|
||||
|
||||
const snapshot = new ExecutionSnapshot(
|
||||
@@ -280,6 +295,18 @@ async function executeWebhookJobInternal(
|
||||
// No changes to process
|
||||
logger.info(`[${requestId}] No Airtable changes to process`)
|
||||
|
||||
// Start logging session so the complete call has a log entry to update
|
||||
await loggingSession.safeStart({
|
||||
userId: payload.userId,
|
||||
workspaceId: workspaceId || '',
|
||||
variables: {},
|
||||
triggerData: {
|
||||
isTest: payload.testMode === true,
|
||||
executionTarget: payload.executionTarget || 'deployed',
|
||||
},
|
||||
deploymentVersionId,
|
||||
})
|
||||
|
||||
await loggingSession.safeComplete({
|
||||
endedAt: new Date().toISOString(),
|
||||
totalDurationMs: 0,
|
||||
@@ -325,6 +352,19 @@ async function executeWebhookJobInternal(
|
||||
|
||||
if (!input && payload.provider === 'whatsapp') {
|
||||
logger.info(`[${requestId}] No messages in WhatsApp payload, skipping execution`)
|
||||
|
||||
// Start logging session so the complete call has a log entry to update
|
||||
await loggingSession.safeStart({
|
||||
userId: payload.userId,
|
||||
workspaceId: workspaceId || '',
|
||||
variables: {},
|
||||
triggerData: {
|
||||
isTest: payload.testMode === true,
|
||||
executionTarget: payload.executionTarget || 'deployed',
|
||||
},
|
||||
deploymentVersionId,
|
||||
})
|
||||
|
||||
await loggingSession.safeComplete({
|
||||
endedAt: new Date().toISOString(),
|
||||
totalDurationMs: 0,
|
||||
@@ -444,6 +484,13 @@ async function executeWebhookJobInternal(
|
||||
useDraftState: false,
|
||||
startTime: new Date().toISOString(),
|
||||
isClientSession: false,
|
||||
workflowStateOverride: {
|
||||
blocks,
|
||||
edges,
|
||||
loops: loops || {},
|
||||
parallels: parallels || {},
|
||||
deploymentVersionId,
|
||||
},
|
||||
}
|
||||
|
||||
const snapshot = new ExecutionSnapshot(metadata, workflow, input || {}, workflowVariables, [])
|
||||
@@ -495,7 +542,6 @@ async function executeWebhookJobInternal(
|
||||
})
|
||||
|
||||
try {
|
||||
// Ensure logging session is started (safe to call multiple times)
|
||||
await loggingSession.safeStart({
|
||||
userId: payload.userId,
|
||||
workspaceId: '', // May not be available for early errors
|
||||
@@ -504,6 +550,7 @@ async function executeWebhookJobInternal(
|
||||
isTest: payload.testMode === true,
|
||||
executionTarget: payload.executionTarget || 'deployed',
|
||||
},
|
||||
deploymentVersionId, // Pass if available (undefined for early errors)
|
||||
})
|
||||
|
||||
const executionResult = (error?.executionResult as ExecutionResult | undefined) || {
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface ExecutionMetadata {
|
||||
edges: Edge[]
|
||||
loops?: Record<string, any>
|
||||
parallels?: Record<string, any>
|
||||
deploymentVersionId?: string // ID of deployment version if this is deployed state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,11 +113,13 @@ export class ExecutionLogger implements IExecutionLoggerService {
|
||||
trigger: ExecutionTrigger
|
||||
environment: ExecutionEnvironment
|
||||
workflowState: WorkflowState
|
||||
deploymentVersionId?: string
|
||||
}): Promise<{
|
||||
workflowLog: WorkflowExecutionLog
|
||||
snapshot: WorkflowExecutionSnapshot
|
||||
}> {
|
||||
const { workflowId, executionId, trigger, environment, workflowState } = params
|
||||
const { workflowId, executionId, trigger, environment, workflowState, deploymentVersionId } =
|
||||
params
|
||||
|
||||
logger.debug(`Starting workflow execution ${executionId} for workflow ${workflowId}`)
|
||||
|
||||
@@ -168,6 +170,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
|
||||
workflowId,
|
||||
executionId,
|
||||
stateSnapshotId: snapshotResult.snapshot.id,
|
||||
deploymentVersionId: deploymentVersionId ?? null,
|
||||
level: 'info',
|
||||
trigger: trigger.type,
|
||||
startedAt: startTime,
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface SessionStartParams {
|
||||
variables?: Record<string, string>
|
||||
triggerData?: Record<string, unknown>
|
||||
skipLogCreation?: boolean // For resume executions - reuse existing log entry
|
||||
deploymentVersionId?: string // ID of the deployment version used (null for manual/editor executions)
|
||||
}
|
||||
|
||||
export interface SessionCompleteParams {
|
||||
@@ -65,7 +66,8 @@ export class LoggingSession {
|
||||
}
|
||||
|
||||
async start(params: SessionStartParams = {}): Promise<void> {
|
||||
const { userId, workspaceId, variables, triggerData, skipLogCreation } = params
|
||||
const { userId, workspaceId, variables, triggerData, skipLogCreation, deploymentVersionId } =
|
||||
params
|
||||
|
||||
try {
|
||||
this.trigger = createTriggerObject(this.triggerType, triggerData)
|
||||
@@ -86,6 +88,7 @@ export class LoggingSession {
|
||||
trigger: this.trigger,
|
||||
environment: this.environment,
|
||||
workflowState: this.workflowState,
|
||||
deploymentVersionId,
|
||||
})
|
||||
|
||||
if (this.requestId) {
|
||||
@@ -267,7 +270,7 @@ export class LoggingSession {
|
||||
|
||||
// Fallback: create a minimal logging session without full workflow state
|
||||
try {
|
||||
const { userId, workspaceId, variables, triggerData } = params
|
||||
const { userId, workspaceId, variables, triggerData, deploymentVersionId } = params
|
||||
this.trigger = createTriggerObject(this.triggerType, triggerData)
|
||||
this.environment = createEnvironmentObject(
|
||||
this.workflowId,
|
||||
@@ -290,6 +293,7 @@ export class LoggingSession {
|
||||
trigger: this.trigger,
|
||||
environment: this.environment,
|
||||
workflowState: this.workflowState,
|
||||
deploymentVersionId,
|
||||
})
|
||||
|
||||
if (this.requestId) {
|
||||
|
||||
@@ -111,6 +111,7 @@ export async function executeWorkflowCore(
|
||||
let edges: Edge[]
|
||||
let loops
|
||||
let parallels
|
||||
let deploymentVersionId: string | undefined
|
||||
|
||||
// Use workflowStateOverride if provided (for diff workflows)
|
||||
if (metadata.workflowStateOverride) {
|
||||
@@ -118,6 +119,7 @@ export async function executeWorkflowCore(
|
||||
edges = metadata.workflowStateOverride.edges
|
||||
loops = metadata.workflowStateOverride.loops || {}
|
||||
parallels = metadata.workflowStateOverride.parallels || {}
|
||||
deploymentVersionId = metadata.workflowStateOverride.deploymentVersionId
|
||||
|
||||
logger.info(`[${requestId}] Using workflow state override (diff workflow execution)`, {
|
||||
blocksCount: Object.keys(blocks).length,
|
||||
@@ -144,6 +146,7 @@ export async function executeWorkflowCore(
|
||||
edges = deployedData.edges
|
||||
loops = deployedData.loops
|
||||
parallels = deployedData.parallels
|
||||
deploymentVersionId = deployedData.deploymentVersionId
|
||||
|
||||
logger.info(`[${requestId}] Using deployed workflow state (deployed execution)`)
|
||||
}
|
||||
@@ -174,6 +177,7 @@ export async function executeWorkflowCore(
|
||||
workspaceId: providedWorkspaceId,
|
||||
variables,
|
||||
skipLogCreation, // Skip if resuming an existing execution
|
||||
deploymentVersionId, // Only set for deployed executions
|
||||
})
|
||||
|
||||
// Process block states with env var substitution using pre-decrypted values
|
||||
|
||||
@@ -40,6 +40,10 @@ export interface NormalizedWorkflowData {
|
||||
isFromNormalizedTables: boolean // Flag to indicate source (true = normalized tables, false = deployed state)
|
||||
}
|
||||
|
||||
export interface DeployedWorkflowData extends NormalizedWorkflowData {
|
||||
deploymentVersionId: string
|
||||
}
|
||||
|
||||
export async function blockExistsInDeployment(
|
||||
workflowId: string,
|
||||
blockId: string
|
||||
@@ -68,12 +72,11 @@ export async function blockExistsInDeployment(
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadDeployedWorkflowState(
|
||||
workflowId: string
|
||||
): Promise<NormalizedWorkflowData> {
|
||||
export async function loadDeployedWorkflowState(workflowId: string): Promise<DeployedWorkflowData> {
|
||||
try {
|
||||
const [active] = await db
|
||||
.select({
|
||||
id: workflowDeploymentVersion.id,
|
||||
state: workflowDeploymentVersion.state,
|
||||
createdAt: workflowDeploymentVersion.createdAt,
|
||||
})
|
||||
@@ -99,6 +102,7 @@ export async function loadDeployedWorkflowState(
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
isFromNormalizedTables: false,
|
||||
deploymentVersionId: active.id,
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error loading deployed workflow state ${workflowId}:`, error)
|
||||
|
||||
@@ -100,6 +100,8 @@ export interface WorkflowLog {
|
||||
id: string
|
||||
workflowId: string
|
||||
executionId?: string | null
|
||||
deploymentVersion?: number | null
|
||||
deploymentVersionName?: string | null
|
||||
level: string
|
||||
duration: string | null
|
||||
trigger: string | null
|
||||
|
||||
@@ -4,8 +4,8 @@ import { persist } from 'zustand/middleware'
|
||||
/**
|
||||
* Width constraints for the log details panel.
|
||||
*/
|
||||
export const MIN_LOG_DETAILS_WIDTH = 340
|
||||
export const DEFAULT_LOG_DETAILS_WIDTH = 340
|
||||
export const MIN_LOG_DETAILS_WIDTH = 400
|
||||
export const DEFAULT_LOG_DETAILS_WIDTH = 400
|
||||
|
||||
/**
|
||||
* Returns the maximum log details panel width (50vw).
|
||||
|
||||
@@ -20,6 +20,8 @@ interface TerminalState {
|
||||
setOutputPanelWidth: (width: number) => void
|
||||
openOnRun: boolean
|
||||
setOpenOnRun: (open: boolean) => void
|
||||
wrapText: boolean
|
||||
setWrapText: (wrap: boolean) => void
|
||||
/**
|
||||
* Indicates whether the terminal is currently being resized via mouse drag.
|
||||
*
|
||||
@@ -35,8 +37,6 @@ interface TerminalState {
|
||||
* @param isResizing - True while the terminal is being resized.
|
||||
*/
|
||||
setIsResizing: (isResizing: boolean) => void
|
||||
// displayMode: DisplayMode
|
||||
// setDisplayMode: (mode: DisplayMode) => void
|
||||
_hasHydrated: boolean
|
||||
setHasHydrated: (hasHydrated: boolean) => void
|
||||
}
|
||||
@@ -119,10 +119,15 @@ export const useTerminalStore = create<TerminalState>()(
|
||||
setOpenOnRun: (open) => {
|
||||
set({ openOnRun: open })
|
||||
},
|
||||
// displayMode: DEFAULT_DISPLAY_MODE,
|
||||
// setDisplayMode: (mode) => {
|
||||
// set({ displayMode: mode })
|
||||
// },
|
||||
wrapText: true,
|
||||
/**
|
||||
* Enables or disables text wrapping in the output panel.
|
||||
*
|
||||
* @param wrap - Whether output text should wrap.
|
||||
*/
|
||||
setWrapText: (wrap) => {
|
||||
set({ wrapText: wrap })
|
||||
},
|
||||
/**
|
||||
* Indicates whether the terminal store has finished client-side hydration.
|
||||
*/
|
||||
|
||||
3
packages/db/migrations/0121_new_mantis.sql
Normal file
3
packages/db/migrations/0121_new_mantis.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "workflow_execution_logs" ADD COLUMN "deployment_version_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "workflow_execution_logs" ADD CONSTRAINT "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk" FOREIGN KEY ("deployment_version_id") REFERENCES "public"."workflow_deployment_version"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "workflow_execution_logs_deployment_version_id_idx" ON "workflow_execution_logs" USING btree ("deployment_version_id");
|
||||
7816
packages/db/migrations/meta/0121_snapshot.json
Normal file
7816
packages/db/migrations/meta/0121_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -841,6 +841,13 @@
|
||||
"when": 1765339999291,
|
||||
"tag": "0120_illegal_moon_knight",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 121,
|
||||
"version": "7",
|
||||
"when": 1765434895472,
|
||||
"tag": "0121_new_mantis",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -293,6 +293,10 @@ export const workflowExecutionLogs = pgTable(
|
||||
stateSnapshotId: text('state_snapshot_id')
|
||||
.notNull()
|
||||
.references(() => workflowExecutionSnapshots.id),
|
||||
deploymentVersionId: text('deployment_version_id').references(
|
||||
() => workflowDeploymentVersion.id,
|
||||
{ onDelete: 'set null' }
|
||||
),
|
||||
|
||||
level: text('level').notNull(), // 'info', 'error'
|
||||
trigger: text('trigger').notNull(), // 'api', 'webhook', 'schedule', 'manual', 'chat'
|
||||
@@ -311,6 +315,9 @@ export const workflowExecutionLogs = pgTable(
|
||||
stateSnapshotIdIdx: index('workflow_execution_logs_state_snapshot_id_idx').on(
|
||||
table.stateSnapshotId
|
||||
),
|
||||
deploymentVersionIdIdx: index('workflow_execution_logs_deployment_version_id_idx').on(
|
||||
table.deploymentVersionId
|
||||
),
|
||||
triggerIdx: index('workflow_execution_logs_trigger_idx').on(table.trigger),
|
||||
levelIdx: index('workflow_execution_logs_level_idx').on(table.level),
|
||||
startedAtIdx: index('workflow_execution_logs_started_at_idx').on(table.startedAt),
|
||||
|
||||
Reference in New Issue
Block a user