LogDrilldown.stories.tsx•16.3 kB
import { Meta, StoryObj } from "@storybook/nextjs";
import { LogDrilldown } from "@common/features/logs/components/LogDrilldown";
import { UdfLog, LogOutcome, UdfLogOutput } from "@common/lib/useLogs";
import { UsageStats } from "system-udfs/convex/_system/frontend/common";
import { functionIdentifierValue } from "@common/lib/functions/generateFileTree";
import { formatDateTime } from "@common/lib/format";
import { useState } from "react";
// Wrapper component that manages execution state
function ExecutionWrapper({
children,
initialExecutionId,
}: {
children: (props: {
executionId?: string;
onGoBack: () => void;
onGoToExecutionId: (executionId: string, functionName: string) => void;
}) => React.ReactNode;
initialExecutionId?: string;
}) {
const [currentExecutionId, setCurrentExecutionId] = useState<
string | undefined
>(initialExecutionId);
const handleGoBack = () => {
setCurrentExecutionId(undefined);
};
const handleGoToExecutionId = (executionId: string, functionName: string) => {
setCurrentExecutionId(executionId);
};
return (
<>
{children({
executionId: currentExecutionId,
onGoBack: handleGoBack,
onGoToExecutionId: handleGoToExecutionId,
})}
</>
);
}
const meta = {
component: LogDrilldown,
parameters: {
layout: "fullscreen",
},
} satisfies Meta<typeof LogDrilldown>;
export default meta;
type Story = StoryObj<typeof meta>;
// Mock data generators
const createMockLogCommon = (
overrides: Partial<UdfLog> = {},
): Partial<UdfLog> => {
const timestamp = overrides.timestamp || Date.now();
return {
id: "log-123",
udfType: "Query",
localizedTimestamp: formatDateTime(new Date(timestamp)),
timestamp,
call: functionIdentifierValue("messages:list"),
requestId: "req-abc123",
executionId: "exec-def456",
...overrides,
};
};
const createMockOutcomeLog = (overrides: Partial<UdfLog> = {}): UdfLog =>
({
...createMockLogCommon(overrides),
kind: "outcome",
outcome: { status: "success", statusCode: null } as LogOutcome,
executionTimeMs: 125.5,
caller: "SyncWorker",
environment: "isolate",
identityType: "user",
parentExecutionId: null,
...overrides,
}) as UdfLog;
const createMockLogEntry = (overrides: Partial<UdfLog> = {}): UdfLog =>
({
...createMockLogCommon(overrides),
kind: "log",
output: {
isTruncated: false,
messages: ["Function executed successfully"],
timestamp: Date.now(),
level: "INFO",
} as UdfLogOutput,
...overrides,
}) as UdfLog;
const mockLogs: UdfLog[] = [
// Root function logs first
createMockLogEntry({
id: "log-1",
call: functionIdentifierValue("messages:send"),
executionId: "exec-root-123",
timestamp: Date.now() - 4500,
output: {
isTruncated: false,
messages: ["Starting message send process"],
level: "INFO",
} as UdfLogOutput,
}),
// Child function (query) logs
createMockLogEntry({
id: "log-2",
call: functionIdentifierValue("messages:list"),
executionId: "exec-child-456",
timestamp: Date.now() - 3900,
output: {
isTruncated: false,
messages: ["Retrieved 5 messages from database"],
level: "INFO",
} as UdfLogOutput,
}),
// Child function (query) outcome - after its logs
createMockOutcomeLog({
id: "log-3",
call: functionIdentifierValue("messages:list"),
udfType: "Query",
executionId: "exec-child-456",
parentExecutionId: "exec-root-123",
cachedResult: true,
caller: "SyncWorker",
environment: "isolate",
identityType: "user",
timestamp: Date.now() - 3800,
executionTimeMs: 15.2,
usageStats: {
actionMemoryUsedMb: null,
databaseReadBytes: 1024,
databaseWriteBytes: 0,
databaseReadDocuments: 5,
storageReadBytes: 0,
storageWriteBytes: 0,
vectorIndexReadBytes: 0,
vectorIndexWriteBytes: 0,
} as UsageStats,
}),
// Action execution outcome
createMockOutcomeLog({
id: "log-4",
call: functionIdentifierValue("messages:processMessage"),
udfType: "Action",
executionId: "exec-action-789",
parentExecutionId: "exec-root-123",
caller: "Action",
environment: "node",
identityType: "user",
timestamp: Date.now() - 3000,
executionTimeMs: 250.8,
usageStats: {
actionMemoryUsedMb: 64,
databaseReadBytes: 512,
databaseWriteBytes: 256,
databaseReadDocuments: 2,
storageReadBytes: 2048,
storageWriteBytes: 1024,
vectorIndexReadBytes: 0,
vectorIndexWriteBytes: 0,
} as UsageStats,
}),
// Error case outcome
createMockOutcomeLog({
id: "log-5",
call: functionIdentifierValue("messages:validateInput"),
udfType: "Mutation",
executionId: "exec-error-def",
parentExecutionId: "exec-root-123",
caller: "SyncWorker",
environment: "isolate",
identityType: "user",
timestamp: Date.now() - 2000,
executionTimeMs: 45.1,
outcome: { status: "failure", statusCode: null } as LogOutcome,
error: "ValidationError: Message body cannot be empty",
}),
// Root function outcome - last, after all child functions complete
createMockOutcomeLog({
id: "log-7",
call: functionIdentifierValue("messages:send"),
udfType: "Mutation",
executionId: "exec-root-123",
parentExecutionId: null,
caller: "SyncWorker",
environment: "isolate",
identityType: "user",
timestamp: Date.now() - 1000,
executionTimeMs: 500.5,
usageStats: {
actionMemoryUsedMb: null,
databaseReadBytes: 2048,
databaseWriteBytes: 512,
databaseReadDocuments: 7,
storageReadBytes: 2048,
storageWriteBytes: 1024,
vectorIndexReadBytes: 0,
vectorIndexWriteBytes: 0,
} as UsageStats,
}),
];
// Stories
export const Default: Story = {
render: (args) => (
<ExecutionWrapper initialExecutionId="exec-root-123">
{(navProps) => <LogDrilldown {...args} {...navProps} />}
</ExecutionWrapper>
),
args: {
requestId: "req-abc123",
logs: mockLogs,
onClose: () => {},
},
};
export const WithCachedQuery: Story = {
render: (args) => (
<ExecutionWrapper initialExecutionId="exec-cached-456">
{(navProps) => <LogDrilldown {...args} {...navProps} />}
</ExecutionWrapper>
),
args: {
requestId: "req-cached-123",
logs: [
createMockOutcomeLog({
id: "cached-1",
call: functionIdentifierValue("users:getProfile"),
udfType: "Query",
executionId: "exec-cached-456",
parentExecutionId: null,
cachedResult: true,
executionTimeMs: 2.1,
caller: "SyncWorker",
environment: "isolate",
identityType: "user",
}),
createMockLogEntry({
id: "cached-2",
call: functionIdentifierValue("users:getProfile"),
executionId: "exec-cached-456",
output: {
isTruncated: false,
messages: ["Profile retrieved from cache"],
level: "INFO",
} as UdfLogOutput,
}),
],
onClose: () => {},
},
};
export const WithErrorExecution: Story = {
render: (args) => (
<ExecutionWrapper initialExecutionId="exec-error-456">
{(navProps) => <LogDrilldown {...args} {...navProps} />}
</ExecutionWrapper>
),
args: {
requestId: "req-error-123",
logs: [
createMockLogEntry({
id: "error-2",
call: functionIdentifierValue("auth:validateToken"),
executionId: "exec-error-456",
output: {
isTruncated: false,
messages: ["Token validation failed"],
level: "ERROR",
} as UdfLogOutput,
}),
createMockOutcomeLog({
id: "error-1",
call: functionIdentifierValue("auth:validateToken"),
udfType: "Query",
executionId: "exec-error-456",
parentExecutionId: null,
executionTimeMs: 12.3,
caller: "SyncWorker",
environment: "isolate",
identityType: "user",
outcome: { status: "failure", statusCode: null } as LogOutcome,
error: "AuthError: Invalid token signature",
}),
],
onClose: () => {},
},
};
export const HttpActionExecution: Story = {
render: (args) => (
<ExecutionWrapper initialExecutionId="exec-http-456">
{(navProps) => <LogDrilldown {...args} {...navProps} />}
</ExecutionWrapper>
),
args: {
requestId: "req-http-123",
logs: [
createMockOutcomeLog({
id: "http-1",
call: functionIdentifierValue("api:uploadFile"),
udfType: "HttpAction",
executionId: "exec-http-456",
parentExecutionId: null,
executionTimeMs: 1250.7,
caller: "HttpEndpoint",
environment: "node",
identityType: "user",
outcome: { status: "success", statusCode: "201" } as LogOutcome,
}),
createMockLogEntry({
id: "http-2",
call: functionIdentifierValue("api:uploadFile"),
executionId: "exec-http-456",
output: {
isTruncated: false,
messages: ["File uploaded successfully to S3"],
level: "INFO",
} as UdfLogOutput,
}),
],
onClose: () => {},
},
};
export const LongRunningAction: Story = {
render: (args) => (
<ExecutionWrapper initialExecutionId="exec-long-456">
{(navProps) => <LogDrilldown {...args} {...navProps} />}
</ExecutionWrapper>
),
args: {
requestId: "req-long-123",
logs: [
createMockOutcomeLog({
id: "long-1",
call: functionIdentifierValue("background:processLargeDataset"),
udfType: "Action",
executionId: "exec-long-456",
parentExecutionId: null,
executionTimeMs: 15420.9,
caller: "Scheduler",
environment: "node",
identityType: "system",
}),
createMockLogEntry({
id: "long-2",
call: functionIdentifierValue("background:processLargeDataset"),
executionId: "exec-long-456",
output: {
isTruncated: false,
messages: ["Processing 10,000 records..."],
level: "INFO",
} as UdfLogOutput,
}),
createMockLogEntry({
id: "long-3",
call: functionIdentifierValue("background:processLargeDataset"),
executionId: "exec-long-456",
output: {
isTruncated: false,
messages: ["Completed processing in 15.4 seconds"],
level: "INFO",
} as UdfLogOutput,
}),
],
onClose: () => {},
},
};
export const MultipleExecutions: Story = {
render: (args) => (
<ExecutionWrapper initialExecutionId="exec-root-123">
{(navProps) => <LogDrilldown {...args} {...navProps} />}
</ExecutionWrapper>
),
args: {
requestId: "req-multi-123",
logs: mockLogs,
onClose: () => {},
},
};
export const OverviewMode: Story = {
render: (args) => (
<ExecutionWrapper>
{(navProps) => <LogDrilldown {...args} {...navProps} />}
</ExecutionWrapper>
),
args: {
requestId: "req-multi-123",
logs: mockLogs,
onClose: () => {},
},
};
export const IncompleteActionExecution: Story = {
render: (args) => (
<ExecutionWrapper initialExecutionId="exec-incomplete-456">
{(navProps) => <LogDrilldown {...args} {...navProps} />}
</ExecutionWrapper>
),
args: {
requestId: "req-incomplete-123",
logs: [
// Log entries for the running action (no outcome yet)
createMockLogEntry({
id: "incomplete-1",
call: functionIdentifierValue("background:processLargeFile"),
udfType: "Action",
executionId: "exec-incomplete-456",
timestamp: Date.now() - 30000,
output: {
isTruncated: false,
messages: ["Starting file processing..."],
level: "INFO",
} as UdfLogOutput,
}),
createMockLogEntry({
id: "incomplete-2",
call: functionIdentifierValue("background:processLargeFile"),
udfType: "Action",
executionId: "exec-incomplete-456",
timestamp: Date.now() - 25000,
output: {
isTruncated: false,
messages: ["Downloaded file from external API"],
level: "INFO",
} as UdfLogOutput,
}),
// Completed child function calls within the running action
createMockOutcomeLog({
id: "incomplete-3",
call: functionIdentifierValue("files:validateFormat"),
udfType: "Query",
executionId: "exec-child-validation",
parentExecutionId: "exec-incomplete-456",
executionTimeMs: 45.2,
caller: "Action",
environment: "isolate",
identityType: "user",
timestamp: Date.now() - 20000,
}),
createMockLogEntry({
id: "incomplete-4",
call: functionIdentifierValue("files:validateFormat"),
executionId: "exec-child-validation",
timestamp: Date.now() - 19800,
output: {
isTruncated: false,
messages: ["File format validation passed"],
level: "INFO",
} as UdfLogOutput,
}),
createMockOutcomeLog({
id: "incomplete-5",
call: functionIdentifierValue("metadata:extractInfo"),
udfType: "Mutation",
executionId: "exec-child-extract",
parentExecutionId: "exec-incomplete-456",
executionTimeMs: 125.7,
caller: "Action",
environment: "isolate",
identityType: "user",
timestamp: Date.now() - 15000,
}),
createMockLogEntry({
id: "incomplete-6",
call: functionIdentifierValue("metadata:extractInfo"),
executionId: "exec-child-extract",
timestamp: Date.now() - 14800,
output: {
isTruncated: false,
messages: ["Extracted metadata and stored in database"],
level: "INFO",
} as UdfLogOutput,
}),
// Incomplete child function - started but no outcome yet (nested under processLargeFile)
createMockLogEntry({
id: "incomplete-child-1",
call: functionIdentifierValue("storage:uploadChunks"),
udfType: "Action",
executionId: "exec-child-upload",
parentExecutionId: "exec-incomplete-456",
timestamp: Date.now() - 12000,
output: {
isTruncated: false,
messages: ["Starting batch upload of processed chunks..."],
level: "INFO",
} as UdfLogOutput,
}),
createMockLogEntry({
id: "incomplete-child-2",
call: functionIdentifierValue("storage:uploadChunks"),
udfType: "Action",
executionId: "exec-child-upload",
parentExecutionId: "exec-incomplete-456",
timestamp: Date.now() - 8000,
output: {
isTruncated: false,
messages: ["Uploaded 3 of 8 chunks to S3..."],
level: "INFO",
} as UdfLogOutput,
}),
// More recent logs from the still-running action
createMockLogEntry({
id: "incomplete-7",
call: functionIdentifierValue("background:processLargeFile"),
udfType: "Action",
executionId: "exec-incomplete-456",
timestamp: Date.now() - 10000,
output: {
isTruncated: false,
messages: ["Processing chunk 5 of 20..."],
level: "INFO",
} as UdfLogOutput,
}),
createMockLogEntry({
id: "incomplete-8",
call: functionIdentifierValue("background:processLargeFile"),
udfType: "Action",
executionId: "exec-incomplete-456",
timestamp: Date.now() - 5000,
output: {
isTruncated: false,
messages: ["Processing chunk 8 of 20..."],
level: "INFO",
} as UdfLogOutput,
}),
createMockLogEntry({
id: "incomplete-9",
call: functionIdentifierValue("background:processLargeFile"),
udfType: "Action",
executionId: "exec-incomplete-456",
timestamp: Date.now() - 2000,
output: {
isTruncated: false,
messages: ["Processing chunk 12 of 20..."],
level: "INFO",
} as UdfLogOutput,
}),
// Note: No outcome log for the root action - it's still running
],
onClose: () => {},
},
};