refactor(front): move workflow run step logs into workflow-actions (#23841)
Follow-up on unapplied review feedback from #21142. ### Folder structure @thomtrp: > We now have a folder workflow-run/observability, but we have workflow-run related components in workflow-actions folder already. I would avoid that workflow-run/observability folder. The `workflow-run/` folder held nothing but `observability/`, so it is removed entirely and each step log detail component now sits with its action: | File | New location | | --- | --- | | `WorkflowRunStepLogsAiAgentDetail.tsx`, `WorkflowRunStepLogsToolCallRow.tsx` | `workflow-actions/ai-agent-action/components/` | | `WorkflowRunStepLogsCodeDetail.tsx` | `workflow-actions/code-action/components/` | | `WorkflowRunStepLogsHttpRequestDetail.tsx` | `workflow-actions/http-request-action/components/` | | `WorkflowRunStepLogsDetail.tsx`, `WorkflowRunStepLogsEntries.tsx`, `WorkflowRunStepLogsEmailDetail.tsx`, `workflowRunStepLogsStyles.ts` | `workflow-actions/components/` | | `formatDuration.ts`, `formatBytes.ts` | `workflow-actions/utils/` | The email detail stays in the shared `components/` folder since there is no dedicated send-email action folder on the front end (email editing lives at the root as `WorkflowEditActionEmailBase.tsx`). `workflowRunStepLogsStyles.ts` goes next to the shared components rather than `utils/`, since it is styled components and not utils. Also folds in @FelixMalfait's `2 export in 1 file` comment: `workflowRunStepLogsFormatters.ts` is split into `formatDuration.ts` and `formatBytes.ts`. ### AI comments @thomtrp: > same, let's not keep AI comments Removes every comment #21142 introduced, across the front end, server and shared packages: - `WorkflowRunStepLogsEntries.tsx` - the `onlyLatestIteration` prop block - `workflow-run-step-log-schema.ts` - the transport-failure note and the permissive-schema rationale - `strip-ansi-escapes.util.spec.ts`, `build-http-request-step-log.util.spec.ts`, `truncate-string-to-utf8-byte-budget.spec.ts`, `agent-async-executor.service.spec.ts` - the byte-vs-char and pre-fix-behaviour commentary No behaviour change. Locale catalogs are deliberately left untouched; the next i18n run picks up the new source paths. ### Still open from #21142, not covered here - `persistStepLog` try/catch duplicated across the code, tool-backed and ai-agent actions - `draft-email-tool` returning both `sanitizedHtmlBody` and `plainTextBody` - storing both `totalCostInDollars` and `creditsUsedMicro` - the byte-budget truncation utilities being over-engineered - `strip-ansi-escapes` being local to application logs
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@ import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
|||||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||||
import { WorkflowRunStepLogsDetail } from '@/workflow/workflow-run/observability/WorkflowRunStepLogsDetail';
|
import { WorkflowRunStepLogsDetail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowRunStepLogsDetail';
|
||||||
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
|
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
|
||||||
import { WorkflowRunStepNodeDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepNodeDetail';
|
import { WorkflowRunStepNodeDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepNodeDetail';
|
||||||
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
|
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
|
||||||
|
|||||||
+3
-3
@@ -13,7 +13,7 @@ import {
|
|||||||
} from 'twenty-ui/icon';
|
} from 'twenty-ui/icon';
|
||||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
|
|
||||||
import { formatDuration } from '@/workflow/workflow-run/observability/workflowRunStepLogsFormatters';
|
import { formatDuration } from '@/workflow/workflow-steps/workflow-actions/utils/formatDuration';
|
||||||
import {
|
import {
|
||||||
StyledBadgeGroup,
|
StyledBadgeGroup,
|
||||||
StyledEmptyHint,
|
StyledEmptyHint,
|
||||||
@@ -27,8 +27,8 @@ import {
|
|||||||
StyledSummaryCard,
|
StyledSummaryCard,
|
||||||
StyledSummaryHeader,
|
StyledSummaryHeader,
|
||||||
StyledTitle,
|
StyledTitle,
|
||||||
} from '@/workflow/workflow-run/observability/workflowRunStepLogsStyles';
|
} from '@/workflow/workflow-steps/workflow-actions/components/workflowRunStepLogsStyles';
|
||||||
import { WorkflowRunStepLogsToolCallRow } from '@/workflow/workflow-run/observability/WorkflowRunStepLogsToolCallRow';
|
import { WorkflowRunStepLogsToolCallRow } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowRunStepLogsToolCallRow';
|
||||||
import { formatNumber } from '~/utils/format/formatNumber';
|
import { formatNumber } from '~/utils/format/formatNumber';
|
||||||
|
|
||||||
const StyledModelBadge = styled.span`
|
const StyledModelBadge = styled.span`
|
||||||
+2
-2
@@ -11,7 +11,7 @@ import {
|
|||||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
|
|
||||||
import { MONOSPACE_FONT_FAMILY } from '@/ui/theme/constants/MonospaceFontFamily';
|
import { MONOSPACE_FONT_FAMILY } from '@/ui/theme/constants/MonospaceFontFamily';
|
||||||
import { formatDuration } from '@/workflow/workflow-run/observability/workflowRunStepLogsFormatters';
|
import { formatDuration } from '@/workflow/workflow-steps/workflow-actions/utils/formatDuration';
|
||||||
import {
|
import {
|
||||||
StyledErrorCard,
|
StyledErrorCard,
|
||||||
StyledErrorMessageText,
|
StyledErrorMessageText,
|
||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
StyledSummaryCard,
|
StyledSummaryCard,
|
||||||
StyledSummaryHeader,
|
StyledSummaryHeader,
|
||||||
StyledTitle,
|
StyledTitle,
|
||||||
} from '@/workflow/workflow-run/observability/workflowRunStepLogsStyles';
|
} from '@/workflow/workflow-steps/workflow-actions/components/workflowRunStepLogsStyles';
|
||||||
|
|
||||||
const StyledErrorHeader = styled.div`
|
const StyledErrorHeader = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
+5
-5
@@ -10,11 +10,11 @@ import { type JsonValue } from 'type-fest';
|
|||||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||||
import { useWorkflowRunStepLog } from '@/workflow/hooks/useWorkflowRunStepLog';
|
import { useWorkflowRunStepLog } from '@/workflow/hooks/useWorkflowRunStepLog';
|
||||||
import { WorkflowRunStepLogsAiAgentDetail } from '@/workflow/workflow-run/observability/WorkflowRunStepLogsAiAgentDetail';
|
import { WorkflowRunStepLogsAiAgentDetail } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowRunStepLogsAiAgentDetail';
|
||||||
import { WorkflowRunStepLogsCodeDetail } from '@/workflow/workflow-run/observability/WorkflowRunStepLogsCodeDetail';
|
import { WorkflowRunStepLogsCodeDetail } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowRunStepLogsCodeDetail';
|
||||||
import { WorkflowRunStepLogsEmailDetail } from '@/workflow/workflow-run/observability/WorkflowRunStepLogsEmailDetail';
|
import { WorkflowRunStepLogsEmailDetail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowRunStepLogsEmailDetail';
|
||||||
import { WorkflowRunStepLogsEntries } from '@/workflow/workflow-run/observability/WorkflowRunStepLogsEntries';
|
import { WorkflowRunStepLogsEntries } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowRunStepLogsEntries';
|
||||||
import { WorkflowRunStepLogsHttpRequestDetail } from '@/workflow/workflow-run/observability/WorkflowRunStepLogsHttpRequestDetail';
|
import { WorkflowRunStepLogsHttpRequestDetail } from '@/workflow/workflow-steps/workflow-actions/http-request-action/components/WorkflowRunStepLogsHttpRequestDetail';
|
||||||
import { getIsDescendantOfIterator } from '@/workflow/workflow-steps/utils/getIsDescendantOfIterator';
|
import { getIsDescendantOfIterator } from '@/workflow/workflow-steps/utils/getIsDescendantOfIterator';
|
||||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||||
|
|
||||||
+3
-5
@@ -11,10 +11,8 @@ import {
|
|||||||
} from 'twenty-ui/icon';
|
} from 'twenty-ui/icon';
|
||||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
|
|
||||||
import {
|
import { formatBytes } from '@/workflow/workflow-steps/workflow-actions/utils/formatBytes';
|
||||||
formatBytes,
|
import { formatDuration } from '@/workflow/workflow-steps/workflow-actions/utils/formatDuration';
|
||||||
formatDuration,
|
|
||||||
} from '@/workflow/workflow-run/observability/workflowRunStepLogsFormatters';
|
|
||||||
import {
|
import {
|
||||||
StyledBadgeGroup,
|
StyledBadgeGroup,
|
||||||
StyledBodyMeta,
|
StyledBodyMeta,
|
||||||
@@ -32,7 +30,7 @@ import {
|
|||||||
StyledSummaryCard,
|
StyledSummaryCard,
|
||||||
StyledSummaryHeader,
|
StyledSummaryHeader,
|
||||||
StyledTitle,
|
StyledTitle,
|
||||||
} from '@/workflow/workflow-run/observability/workflowRunStepLogsStyles';
|
} from '@/workflow/workflow-steps/workflow-actions/components/workflowRunStepLogsStyles';
|
||||||
|
|
||||||
const StyledModeBadge = styled.span`
|
const StyledModeBadge = styled.span`
|
||||||
background: ${themeCssVariables.background.transparent.light};
|
background: ${themeCssVariables.background.transparent.light};
|
||||||
+1
-4
@@ -7,7 +7,7 @@ import { MONOSPACE_FONT_FAMILY } from '@/ui/theme/constants/MonospaceFontFamily'
|
|||||||
import {
|
import {
|
||||||
StyledSection,
|
StyledSection,
|
||||||
StyledSectionTitle,
|
StyledSectionTitle,
|
||||||
} from '@/workflow/workflow-run/observability/workflowRunStepLogsStyles';
|
} from '@/workflow/workflow-steps/workflow-actions/components/workflowRunStepLogsStyles';
|
||||||
|
|
||||||
const StyledEntriesList = styled.div`
|
const StyledEntriesList = styled.div`
|
||||||
background: ${themeCssVariables.background.tertiary};
|
background: ${themeCssVariables.background.tertiary};
|
||||||
@@ -77,9 +77,6 @@ export const WorkflowRunStepLogsEntries = ({
|
|||||||
onlyLatestIteration = false,
|
onlyLatestIteration = false,
|
||||||
}: {
|
}: {
|
||||||
entries: WorkflowRunStepLog['entries'];
|
entries: WorkflowRunStepLog['entries'];
|
||||||
// Set when the parent step lives inside an iterator loop: each iteration
|
|
||||||
// overwrites the same `stepLogs[stepId]` key, so what we render here is
|
|
||||||
// only the latest iteration's entries — not a cumulative view.
|
|
||||||
onlyLatestIteration?: boolean;
|
onlyLatestIteration?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
+3
-5
@@ -13,10 +13,8 @@ import {
|
|||||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
|
|
||||||
import { MONOSPACE_FONT_FAMILY } from '@/ui/theme/constants/MonospaceFontFamily';
|
import { MONOSPACE_FONT_FAMILY } from '@/ui/theme/constants/MonospaceFontFamily';
|
||||||
import {
|
import { formatBytes } from '@/workflow/workflow-steps/workflow-actions/utils/formatBytes';
|
||||||
formatBytes,
|
import { formatDuration } from '@/workflow/workflow-steps/workflow-actions/utils/formatDuration';
|
||||||
formatDuration,
|
|
||||||
} from '@/workflow/workflow-run/observability/workflowRunStepLogsFormatters';
|
|
||||||
import {
|
import {
|
||||||
StyledBadgeGroup,
|
StyledBadgeGroup,
|
||||||
StyledBodyMeta,
|
StyledBodyMeta,
|
||||||
@@ -34,7 +32,7 @@ import {
|
|||||||
StyledSummaryCard,
|
StyledSummaryCard,
|
||||||
StyledSummaryHeader,
|
StyledSummaryHeader,
|
||||||
StyledTitle,
|
StyledTitle,
|
||||||
} from '@/workflow/workflow-run/observability/workflowRunStepLogsStyles';
|
} from '@/workflow/workflow-steps/workflow-actions/components/workflowRunStepLogsStyles';
|
||||||
|
|
||||||
const StyledMethodBadge = styled.span<{ method: string }>`
|
const StyledMethodBadge = styled.span<{ method: string }>`
|
||||||
background: ${themeCssVariables.background.transparent.light};
|
background: ${themeCssVariables.background.transparent.light};
|
||||||
-8
@@ -1,11 +1,3 @@
|
|||||||
export const formatDuration = (ms: number): string => {
|
|
||||||
if (ms < 1000) {
|
|
||||||
return `${ms}ms`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${(ms / 1000).toFixed(1)}s`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatBytes = (bytes: number): string => {
|
export const formatBytes = (bytes: number): string => {
|
||||||
if (bytes < 1024) {
|
if (bytes < 1024) {
|
||||||
return `${bytes} B`;
|
return `${bytes} B`;
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
export const formatDuration = (ms: number): string => {
|
||||||
|
if (ms < 1000) {
|
||||||
|
return `${ms}ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
};
|
||||||
-4
@@ -6,7 +6,6 @@ describe('stripAnsiEscapes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('strips SGR color codes from a yellow value', () => {
|
it('strips SGR color codes from a yellow value', () => {
|
||||||
// What `console.log(chalk.yellow('4 '))` emits.
|
|
||||||
expect(stripAnsiEscapes('\u001B[33m4 \u001B[39m')).toBe('4 ');
|
expect(stripAnsiEscapes('\u001B[33m4 \u001B[39m')).toBe('4 ');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,9 +39,6 @@ describe('stripAnsiEscapes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('leaves untouched the bracket text that survived a missing ESC', () => {
|
it('leaves untouched the bracket text that survived a missing ESC', () => {
|
||||||
// Defensive: if the ESC byte was already stripped upstream, we should not
|
|
||||||
// try to "fix" the bracketed remnants (we cannot tell them apart from real
|
|
||||||
// user text).
|
|
||||||
expect(stripAnsiEscapes('[33m4 [39m')).toBe('[33m4 [39m');
|
expect(stripAnsiEscapes('[33m4 [39m')).toBe('[33m4 [39m');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
-3
@@ -348,7 +348,6 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti
|
|||||||
|
|
||||||
expect(result.nativeWebSearchCallCount).toBe(0);
|
expect(result.nativeWebSearchCallCount).toBe(0);
|
||||||
expect(result.totalCostInDollars).toBeCloseTo(0.0042, 6);
|
expect(result.totalCostInDollars).toBeCloseTo(0.0042, 6);
|
||||||
// credits = dollars * 1_000_000
|
|
||||||
expect(result.creditsUsedMicro).toBe(4200);
|
expect(result.creditsUsedMicro).toBe(4200);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -369,8 +368,6 @@ describe('AgentAsyncExecutorService — workflow agent role-scoped tool resoluti
|
|||||||
],
|
],
|
||||||
usage: {
|
usage: {
|
||||||
...baseUsage,
|
...baseUsage,
|
||||||
// inputTokens (100) is the full prompt: noCache(60) + cacheRead(10) +
|
|
||||||
// cacheCreation(30) — the emitted total must not add the 30 again
|
|
||||||
inputTokenDetails: {
|
inputTokenDetails: {
|
||||||
noCacheTokens: 60,
|
noCacheTokens: 60,
|
||||||
cacheReadTokens: 10,
|
cacheReadTokens: 10,
|
||||||
|
|||||||
-11
@@ -85,12 +85,6 @@ describe('buildHttpRequestStepLog', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('truncates non-ASCII bodies by UTF-8 bytes, not UTF-16 code units', () => {
|
it('truncates non-ASCII bodies by UTF-8 bytes, not UTF-16 code units', () => {
|
||||||
// CJK characters take 3 UTF-8 bytes each but 1 UTF-16 code unit.
|
|
||||||
// Before the byte-aware fix, `redacted.slice(0, 32_000)` on this payload
|
|
||||||
// would emit ~96 KB of UTF-8 (three times the intended cap). After the
|
|
||||||
// fix the truncated payload stays within the cap, plus at most one
|
|
||||||
// U+FFFD replacement char (~3 bytes) for a multi-byte sequence cut at
|
|
||||||
// the boundary.
|
|
||||||
const longCjkPayload = '日'.repeat(40_000);
|
const longCjkPayload = '日'.repeat(40_000);
|
||||||
|
|
||||||
const stepLog = buildHttpRequestStepLog({
|
const stepLog = buildHttpRequestStepLog({
|
||||||
@@ -197,9 +191,6 @@ describe('buildHttpRequestStepLog', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('redacts sensitive query params even when the URL is unparseable (regression)', () => {
|
it('redacts sensitive query params even when the URL is unparseable (regression)', () => {
|
||||||
// Whitespace in the host trips up the WHATWG URL parser, so this URL
|
|
||||||
// is rejected by `new URL()`. Before the fallback was added, the catch
|
|
||||||
// branch returned the raw URL with secrets intact.
|
|
||||||
const unparseableUrl =
|
const unparseableUrl =
|
||||||
'https://api example.com/data?page=1&api_key=AKIA-leaked&token=oauth-leaked&safe=ok';
|
'https://api example.com/data?page=1&api_key=AKIA-leaked&token=oauth-leaked&safe=ok';
|
||||||
|
|
||||||
@@ -226,8 +217,6 @@ describe('buildHttpRequestStepLog', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('redacts percent-encoded sensitive param names in unparseable URLs', () => {
|
it('redacts percent-encoded sensitive param names in unparseable URLs', () => {
|
||||||
// `api%5Fkey` decodes to `api_key` — the fallback must decode before
|
|
||||||
// matching against the sensitive-name set.
|
|
||||||
const unparseableUrl = 'https://api example.com/x?api%5Fkey=leaked';
|
const unparseableUrl = 'https://api example.com/x?api%5Fkey=leaked';
|
||||||
|
|
||||||
expect(() => new URL(unparseableUrl)).toThrow();
|
expect(() => new URL(unparseableUrl)).toThrow();
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ describe('truncateStringToUtf8ByteBudget', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('reports originalBytes in UTF-8 bytes for non-ASCII content', () => {
|
it('reports originalBytes in UTF-8 bytes for non-ASCII content', () => {
|
||||||
// 1 × '日' = 3 UTF-8 bytes but only 1 UTF-16 code unit.
|
|
||||||
const result = truncateStringToUtf8ByteBudget('日'.repeat(1_000), 30);
|
const result = truncateStringToUtf8ByteBudget('日'.repeat(1_000), 30);
|
||||||
|
|
||||||
expect(result.originalBytes).toBe(3_000);
|
expect(result.originalBytes).toBe(3_000);
|
||||||
@@ -48,8 +47,6 @@ describe('truncateStringToUtf8ByteBudget', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('truncates CJK content within budget rather than 3× over (regression)', () => {
|
it('truncates CJK content within budget rather than 3× over (regression)', () => {
|
||||||
// Pre-fix `slice(0, maxBytes)` would have emitted 32_000 chars =
|
|
||||||
// ~96_000 bytes. The byte-aware util must stay close to the cap.
|
|
||||||
const cjk = '日'.repeat(40_000);
|
const cjk = '日'.repeat(40_000);
|
||||||
const cap = 32_000;
|
const cap = 32_000;
|
||||||
|
|
||||||
@@ -59,8 +56,6 @@ describe('truncateStringToUtf8ByteBudget', () => {
|
|||||||
result.value.replace(TRUNCATION_SENTINEL, ''),
|
result.value.replace(TRUNCATION_SENTINEL, ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Allow at most a single U+FFFD substitution (3 UTF-8 bytes) when the
|
|
||||||
// byte boundary falls inside a multi-byte sequence.
|
|
||||||
expect(truncatedPayloadBytes).toBeLessThanOrEqual(cap + 3);
|
expect(truncatedPayloadBytes).toBeLessThanOrEqual(cap + 3);
|
||||||
expect(truncatedPayloadBytes).toBeGreaterThan(cap - 3);
|
expect(truncatedPayloadBytes).toBeGreaterThan(cap - 3);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ const httpRequestStepLogDetailsSchema = z.object({
|
|||||||
bodyBytes: z.number().optional(),
|
bodyBytes: z.number().optional(),
|
||||||
bodyTruncated: z.boolean().optional(),
|
bodyTruncated: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
// `response` is absent for transport-level failures (DNS, timeout, TLS,
|
|
||||||
// etc.) — only `error` is set in that case.
|
|
||||||
response: z
|
response: z
|
||||||
.object({
|
.object({
|
||||||
status: z.number(),
|
status: z.number(),
|
||||||
@@ -130,11 +128,4 @@ export const workflowRunStepLogSchema = z.object({
|
|||||||
sizeBytes: z.number(),
|
sizeBytes: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// We intentionally keep the runtime schema permissive: the column is a
|
|
||||||
// JSONB blob written by the server and the consumers don't validate
|
|
||||||
// individual `details` shapes. The strict per-step type (with the
|
|
||||||
// discriminated `details` union) lives in `WorkflowRunStepLog` and is
|
|
||||||
// applied at the boundaries that *produce* logs (server-side writers).
|
|
||||||
// Tighter zod parsing here would collapse the discriminated union to `{}`
|
|
||||||
// when inferred through `z.record`, breaking front-end indexing.
|
|
||||||
export const workflowRunStepLogsSchema = z.record(z.string(), z.unknown());
|
export const workflowRunStepLogsSchema = z.record(z.string(), z.unknown());
|
||||||
|
|||||||
Reference in New Issue
Block a user