Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
170 changes: 122 additions & 48 deletions apps/sim/lib/core/utils/display-filters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/**
* Type guard to check if an object is a UserFile
*/
const MAX_STRING_LENGTH = 10000
const MAX_DEPTH = 50

function truncateString(value: string, maxLength = MAX_STRING_LENGTH): string {
if (value.length <= maxLength) {
return value
}
return `${value.substring(0, maxLength)}... [truncated ${value.length - maxLength} chars]`
}

export function isUserFile(candidate: unknown): candidate is {
id: string
name: string
Expand All @@ -23,11 +30,6 @@ export function isUserFile(candidate: unknown): candidate is {
)
}

/**
* Filter function that transforms UserFile objects for display
* Removes internal fields: key, context
* Keeps user-friendly fields: id, name, url, size, type
*/
function filterUserFile(data: any): any {
if (isUserFile(data)) {
const { id, name, url, size, type } = data
Expand All @@ -36,50 +38,122 @@ function filterUserFile(data: any): any {
return data
}

/**
* Registry of filter functions to apply to data for cleaner display in logs/console.
* Add new filter functions here to handle additional data types.
*/
const DISPLAY_FILTERS = [
filterUserFile,
// Add more filters here as needed
]

/**
* Generic helper to filter internal/technical fields from data for cleaner display in logs and console.
* Applies all registered filters recursively to the data structure.
*
* To add a new filter:
* 1. Create a filter function that checks and transforms a specific data type
* 2. Add it to the DISPLAY_FILTERS array above
*
* @param data - Data to filter (objects, arrays, primitives)
* @returns Filtered data with internal fields removed
*/
const DISPLAY_FILTERS = [filterUserFile]

export function filterForDisplay(data: any): any {
if (!data || typeof data !== 'object') {
return data
}
const seen = new WeakSet()
return filterForDisplayInternal(data, seen, 0)
}

// Apply all registered filters
const filtered = data
for (const filterFn of DISPLAY_FILTERS) {
const result = filterFn(filtered)
if (result !== filtered) {
// Filter matched and transformed the data
return result
function getObjectType(data: unknown): string {
return Object.prototype.toString.call(data).slice(8, -1)
}

function filterForDisplayInternal(data: any, seen: WeakSet<object>, depth: number): any {
try {
if (data === null || data === undefined) {
return data
}
}

// No filters matched - recursively filter nested structures
if (Array.isArray(filtered)) {
return filtered.map(filterForDisplay)
}
if (typeof data === 'string') {
return truncateString(data)
}

if (typeof data !== 'object') {
return data
}

if (seen.has(data)) {
return '[Circular Reference]'
}

if (depth > MAX_DEPTH) {
return '[Max Depth Exceeded]'
}

const objectType = getObjectType(data)

switch (objectType) {
case 'Date': {
const timestamp = (data as Date).getTime()
if (Number.isNaN(timestamp)) {
return '[Invalid Date]'
}
return (data as Date).toISOString()
}

// Recursively filter object properties
const result: any = {}
for (const [key, value] of Object.entries(filtered)) {
result[key] = filterForDisplay(value)
case 'RegExp':
return (data as RegExp).toString()

case 'URL':
return (data as URL).toString()

case 'Error': {
const err = data as Error
return {
name: err.name,
message: truncateString(err.message),
stack: err.stack ? truncateString(err.stack) : undefined,
}
}

case 'ArrayBuffer':
return `[ArrayBuffer: ${(data as ArrayBuffer).byteLength} bytes]`

case 'Map': {
const obj: Record<string, any> = {}
for (const [key, value] of (data as Map<any, any>).entries()) {
const keyStr = typeof key === 'string' ? key : String(key)
obj[keyStr] = filterForDisplayInternal(value, seen, depth + 1)
}
return obj
}

case 'Set':
return Array.from(data as Set<any>).map((item) =>
filterForDisplayInternal(item, seen, depth + 1)
)

case 'WeakMap':
return '[WeakMap]'

case 'WeakSet':
return '[WeakSet]'

case 'WeakRef':
return '[WeakRef]'

case 'Promise':
return '[Promise]'
}

if (ArrayBuffer.isView(data)) {
return `[${objectType}: ${(data as ArrayBufferView).byteLength} bytes]`
}

seen.add(data)

for (const filterFn of DISPLAY_FILTERS) {
const result = filterFn(data)
if (result !== data) {
return filterForDisplayInternal(result, seen, depth + 1)
}
}

if (Array.isArray(data)) {
return data.map((item) => filterForDisplayInternal(item, seen, depth + 1))
}

const result: Record<string, any> = {}
for (const key of Object.keys(data)) {
try {
result[key] = filterForDisplayInternal(data[key], seen, depth + 1)
} catch {
result[key] = '[Error accessing property]'
}
}
return result
} catch {
return '[Unserializable]'
}
return result
}
81 changes: 74 additions & 7 deletions apps/sim/lib/logs/execution/logging-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,21 +331,88 @@ export class LoggingSession {
try {
await this.complete(params)
} catch (error) {
// Error already logged in complete(), log a summary here
logger.warn(
`[${this.requestId || 'unknown'}] Logging completion failed for execution ${this.executionId} - execution data not persisted`
`[${this.requestId || 'unknown'}] Logging completion failed for execution ${this.executionId} - attempting cost-only fallback`
)

try {
const costSummary = calculateCostSummary(params.traceSpans || [])
const endTime = params.endedAt || new Date().toISOString()
const duration = params.totalDurationMs || 0

await executionLogger.completeWorkflowExecution({
executionId: this.executionId,
endedAt: endTime,
totalDurationMs: duration,
costSummary,
finalOutput: { _fallback: true, error: 'Trace spans too large to store' },
traceSpans: [],
isResume: this.isResume,
})

logger.info(
`[${this.requestId || 'unknown'}] Cost-only fallback succeeded for execution ${this.executionId}`
)
} catch (fallbackError) {
logger.error(
`[${this.requestId || 'unknown'}] Cost-only fallback also failed for execution ${this.executionId}:`,
{
error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
}
)
}
}
}

async safeCompleteWithError(error?: SessionErrorCompleteParams): Promise<void> {
async safeCompleteWithError(params?: SessionErrorCompleteParams): Promise<void> {
try {
await this.completeWithError(error)
} catch (enhancedError) {
// Error already logged in completeWithError(), log a summary here
await this.completeWithError(params)
} catch (error) {
logger.warn(
`[${this.requestId || 'unknown'}] Error logging completion failed for execution ${this.executionId} - execution data not persisted`
`[${this.requestId || 'unknown'}] Error logging completion failed for execution ${this.executionId} - attempting cost-only fallback`
)

try {
const costSummary = params?.traceSpans
? calculateCostSummary(params.traceSpans)
: {
totalCost: BASE_EXECUTION_CHARGE,
totalInputCost: 0,
totalOutputCost: 0,
totalTokens: 0,
totalPromptTokens: 0,
totalCompletionTokens: 0,
baseExecutionCharge: BASE_EXECUTION_CHARGE,
modelCost: 0,
models: {},
}

const endTime = params?.endedAt || new Date().toISOString()
const duration = params?.totalDurationMs || 0

await executionLogger.completeWorkflowExecution({
executionId: this.executionId,
endedAt: endTime,
totalDurationMs: duration,
costSummary,
finalOutput: {
_fallback: true,
error: params?.error?.message || 'Execution failed, trace spans too large to store',
},
traceSpans: [],
})

logger.info(
`[${this.requestId || 'unknown'}] Cost-only fallback succeeded for execution ${this.executionId}`
)
} catch (fallbackError) {
logger.error(
`[${this.requestId || 'unknown'}] Cost-only fallback also failed for execution ${this.executionId}:`,
{
error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
}
)
}
}
}
}
Loading