Skip to content

Commit 5cda510

Browse files
authored
fix(logs): keep checked trace content at every depth and label live projection warnings (#9692)
1 parent 10a9d8a commit 5cda510

4 files changed

Lines changed: 209 additions & 77 deletions

File tree

‎apps/sim/lib/logs/execution/logging-session.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@sim/testing/mocks/billing-usage-reservation.mock'
77
import { encryptionMock, encryptionMockFns } from '@sim/testing/mocks/encryption.mock'
88
import { executionPayloadStoreMock } from '@sim/testing/mocks/execution-payload-store.mock'
9+
import { getMockLogger } from '@sim/testing/mocks/logger.mock'
910
import {
1011
getMockPlatformEvent,
1112
telemetryMock,
@@ -774,6 +775,35 @@ describe('LoggingSession completion retries', () => {
774775
expect(rawLogs[128].output.value).toBe('row-128:raw-secret')
775776
})
776777

778+
it('labels a live block-log projection warning with the live phase', async () => {
779+
const session = new LoggingSession('workflow-1', 'execution-live-phase', 'manual', 'req-1')
780+
session.setResolvedSecretTraceRegistry(
781+
createSecretRegistry([{ plaintext: 'raw-secret', replacement: '{{TOKEN}}' }])
782+
)
783+
784+
await session.projectBlockLogsForDisplay([
785+
{
786+
blockId: 'function-1',
787+
blockName: 'Function 1',
788+
blockType: 'function',
789+
startedAt: '2026-07-01T00:00:00.000Z',
790+
endedAt: '2026-07-01T00:00:00.001Z',
791+
durationMs: 1,
792+
success: true,
793+
executionOrder: 1,
794+
output: { rows: Array.from({ length: 100_001 }, (_, index) => index) },
795+
displayResolvedSecretTraceProvenance: createDisplayProvenance([
796+
{ plaintext: 'raw-secret', replacement: '{{TOKEN}}' },
797+
]),
798+
},
799+
])
800+
801+
expect(getMockLogger('TraceSecretProjection').warn).toHaveBeenCalledWith(
802+
'Omitting trace content that could not be sanitized',
803+
expect.objectContaining({ executionId: 'execution-live-phase', phase: 'live' })
804+
)
805+
})
806+
777807
it('projects a numeric Function result produced by a resolved numeric secret', async () => {
778808
const session = new LoggingSession('workflow-1', 'execution-numeric-secret', 'manual', 'req-1')
779809
session.setResolvedSecretTraceRegistry(

‎apps/sim/lib/logs/execution/logging-session.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,13 @@ export class LoggingSession {
309309

310310
private async projectRawTraceSpans(
311311
traceSpans: TraceSpan[],
312-
registry = this.resolvedSecretTraceRegistry
312+
registry = this.resolvedSecretTraceRegistry,
313+
phase?: 'live'
313314
): Promise<TraceSpan[]> {
314315
return projectTraceSpansForSecrets(traceSpans, {
315316
registry,
316317
store: this.getSecretProjectionStore(),
318+
phase,
317319
})
318320
}
319321

@@ -365,7 +367,8 @@ export class LoggingSession {
365367
output: envelope,
366368
},
367369
],
368-
registry
370+
registry,
371+
'live'
369372
)
370373
const projected = this.readProjectedDisplayContent(projectedSpan?.output)
371374
return this.shouldClearLiveDisplay(registry)
@@ -426,7 +429,8 @@ export class LoggingSession {
426429
...(log.childTraceSpans ? { children: log.childTraceSpans } : {}),
427430
},
428431
],
429-
registry
432+
registry,
433+
'live'
430434
)
431435
const display = this.readProjectedDisplayContent(projectedLog?.output)
432436
displayLogs.push({
@@ -1503,7 +1507,7 @@ export class LoggingSession {
15031507
* structure with no content rather than unprojected values.
15041508
*/
15051509
async projectTraceSpansForLiveDisplay(traceSpans: TraceSpan[]): Promise<TraceSpan[]> {
1506-
return this.projectRawTraceSpans(traceSpans)
1510+
return this.projectRawTraceSpans(traceSpans, undefined, 'live')
15071511
}
15081512

15091513
async safeComplete(params: SessionCompleteParams = {}): Promise<void> {

‎apps/sim/lib/logs/execution/trace-secret-projection.test.ts‎

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,24 +1176,86 @@ describe('projectTraceSpansForSecrets whole-trace content budget', () => {
11761176
expect(projected[1].toolCalls).toHaveLength(50_000)
11771177
})
11781178

1179-
it('withholds the whole top-level span whose descendant crossed the budget', async () => {
1179+
it('keeps checked content under a single workflow root when a descendant crosses the budget', async () => {
11801180
const projected = await projectTraceSpansForSecrets(
11811181
[
1182-
heavySpan('span-a', 'first'),
11831182
createSpan({
1184-
id: 'span-b',
1185-
output: { marker: 'parent top-secret' },
1186-
children: [heavySpan('child-1', 'child one'), heavySpan('child-2', 'child two')],
1183+
id: 'workflow-execution',
1184+
output: { marker: 'root top-secret' },
1185+
children: [
1186+
heavySpan('block-a', 'first top-secret'),
1187+
createSpan({
1188+
id: 'loop',
1189+
output: { marker: 'loop top-secret' },
1190+
children: [
1191+
heavySpan('iteration-1', 'second'),
1192+
heavySpan('iteration-2', 'third top-secret', {
1193+
children: [createSpan({ id: 'nested', output: { marker: 'nested top-secret' } })],
1194+
}),
1195+
createSpan({ id: 'iteration-3', output: { marker: 'fourth top-secret' } }),
1196+
],
1197+
}),
1198+
createSpan({ id: 'block-b', output: { marker: 'fifth top-secret' } }),
1199+
],
11871200
}),
1201+
createSpan({ id: 'after-root', output: { marker: 'sixth top-secret' } }),
11881202
],
11891203
{ registry: SECRET_REGISTRY, store: STORE, allowLargeValueWrites: false }
11901204
)
11911205

1192-
expect(projected[0].output?.marker).toBe('first')
1193-
expect(projected[1]).not.toHaveProperty('output')
1194-
expect(projected[1].contentWithheld).toBe(true)
1195-
expect(projected[1].children?.map((child) => child.id)).toEqual(['child-1', 'child-2'])
1196-
expect(projected[1].children?.every((child) => !Object.hasOwn(child, 'output'))).toBe(true)
1206+
const [root, afterRoot] = projected
1207+
const [blockA, loop, blockB] = root.children ?? []
1208+
const [iteration1, iteration2, iteration3] = loop.children ?? []
1209+
1210+
for (const [kept, marker] of [
1211+
[root, 'root {{API_SECRET}}'],
1212+
[blockA, 'first {{API_SECRET}}'],
1213+
[loop, 'loop {{API_SECRET}}'],
1214+
[iteration1, 'second'],
1215+
] as const) {
1216+
expect(kept.output?.marker).toBe(marker)
1217+
expect(kept).not.toHaveProperty('contentWithheld')
1218+
}
1219+
for (const withheld of [iteration2, iteration2.children?.[0], iteration3, blockB, afterRoot]) {
1220+
expect(withheld).toMatchObject({ contentWithheld: true })
1221+
expect(withheld).not.toHaveProperty('output')
1222+
}
11971223
expect(JSON.stringify(projected)).not.toContain('top-secret')
1224+
expect(warnMock).toHaveBeenCalledWith(
1225+
'Trace content exceeded the projection budget; withholding the remaining content',
1226+
expect.objectContaining({ retainedSpanCount: 4, withheldSpanCount: 5 })
1227+
)
1228+
})
1229+
1230+
it('withholds every span when a descendant fails for any reason but the budget', async () => {
1231+
const projected = await projectTraceSpansForSecrets(
1232+
[
1233+
createSpan({
1234+
id: 'workflow-execution',
1235+
output: { marker: 'root' },
1236+
children: [
1237+
createSpan({ id: 'block-a', output: { marker: 'first' } }),
1238+
createSpan({
1239+
id: 'block-b',
1240+
toolCalls: Array.from({ length: 100_001 }, (_, index) => ({
1241+
name: `tool-${index}`,
1242+
duration: 0,
1243+
startTime: '2026-07-01T00:00:00.000Z',
1244+
endTime: '2026-07-01T00:00:00.000Z',
1245+
status: 'success',
1246+
})),
1247+
}),
1248+
],
1249+
}),
1250+
],
1251+
{ registry: SECRET_REGISTRY, store: STORE, allowLargeValueWrites: false }
1252+
)
1253+
1254+
expect(projected[0]).not.toHaveProperty('output')
1255+
expect(projected[0].children?.[0]).not.toHaveProperty('output')
1256+
expect(warnMock).toHaveBeenCalledWith(
1257+
'Trace secret projection failed; retaining structural spans only',
1258+
expect.objectContaining({ executionId: 'execution-1', phase: 'read' })
1259+
)
11981260
})
11991261
})

0 commit comments

Comments
 (0)