Add API error monitoring tests and error context snapshots for various browsers

- Created error context snapshots for Firefox, WebKit, and Chromium to capture UI state during API error monitoring.
- Implemented e2e tests for API error detection, including console errors, failed API requests, and proxy errors.
- Added functionality to ignore specific API patterns in monitoring assertions.
- Ensured tests validate the monitoring system's ability to detect and report errors effectively.
This commit is contained in:
catlog22
2026-01-31 00:15:59 +08:00
parent f1324a0bc8
commit a0f81f8841
66 changed files with 3112 additions and 3175 deletions

View File

@@ -166,3 +166,13 @@ export type {
UseRulesOptions,
UseRulesReturn,
} from './useCli';
// ========== CLI Execution ==========
export {
useCliExecutionDetail,
cliExecutionKeys,
} from './useCliExecution';
export type {
UseCliExecutionOptions,
UseCliExecutionReturn,
} from './useCliExecution';

View File

@@ -0,0 +1,112 @@
// ========================================
// useCliExecution Hook
// ========================================
// TanStack Query hook for CLI execution details
import { useQuery } from '@tanstack/react-query';
import {
fetchExecutionDetail,
type ConversationRecord,
} from '../lib/api';
// ========== Query Keys ==========
/**
* Query key factory for CLI execution queries
*/
export const cliExecutionKeys = {
all: ['cliExecution'] as const,
details: () => [...cliExecutionKeys.all, 'detail'] as const,
detail: (id: string | null) => [...cliExecutionKeys.details(), id] as const,
};
// ========== Constants ==========
/**
* Default stale time: 5 minutes
* Execution details don't change frequently after completion
*/
const STALE_TIME = 5 * 60 * 1000;
/**
* Cache time: 10 minutes
* Keep cached data available for potential re-use
*/
const GC_TIME = 10 * 60 * 1000;
// ========== Types ==========
export interface UseCliExecutionOptions {
/** Override default stale time (ms) */
staleTime?: number;
/** Override default cache time (ms) */
gcTime?: number;
/** Enable/disable the query */
enabled?: boolean;
}
export interface UseCliExecutionReturn {
/** Execution detail data */
data: ConversationRecord | undefined;
/** Loading state for initial fetch */
isLoading: boolean;
/** Fetching state (initial or refetch) */
isFetching: boolean;
/** Error object if query failed */
error: Error | null;
/** Manually refetch data */
refetch: () => Promise<void>;
}
// ========== Hook ==========
/**
* Hook for fetching CLI execution detail (conversation records)
*
* @param executionId - The CLI execution ID to fetch details for
* @param options - Query options
*
* @example
* ```tsx
* const { data, isLoading, error } = useCliExecutionDetail('exec-123');
* ```
*
* @remarks
* - Query is disabled when executionId is null/undefined
* - Data is cached for 5 minutes by default
* - Auto-refetch is disabled (execution details don't change)
*/
export function useCliExecutionDetail(
executionId: string | null,
options: UseCliExecutionOptions = {}
): UseCliExecutionReturn {
const { staleTime = STALE_TIME, gcTime = GC_TIME, enabled = true } = options;
const query = useQuery<ConversationRecord>({
queryKey: cliExecutionKeys.detail(executionId),
queryFn: () => {
if (!executionId) throw new Error('executionId is required');
return fetchExecutionDetail(executionId);
},
enabled: !!executionId && enabled,
staleTime,
gcTime,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
const refetch = async () => {
await query.refetch();
};
return {
data: query.data,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
};
}

View File

@@ -0,0 +1,37 @@
import { useState, useEffect } from 'react';
/**
* Generic hook for managing localStorage state with SSR safety
* @template T The type of value being stored
* @param key The localStorage key
* @param defaultValue The default value if not in localStorage
* @returns [value, setValue] tuple similar to useState
*/
export function useLocalStorage<T>(key: string, defaultValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(defaultValue);
// Load value from localStorage on mount (for SSR safety)
useEffect(() => {
try {
const item = window.localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.warn(`Failed to load localStorage key "${key}":`, error);
}
}, [key]);
// Update localStorage when value changes
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(`Failed to set localStorage key "${key}":`, error);
}
};
// Return storedValue immediately (it will be hydrated after effect runs)
return [storedValue, setValue];
}

View File

@@ -1,11 +1,11 @@
// ========================================
// useTheme Hook
// ========================================
// Convenient hook for theme management
// Convenient hook for theme management with multi-color scheme support
import { useCallback } from 'react';
import { useAppStore, selectTheme, selectResolvedTheme } from '../stores/appStore';
import type { Theme } from '../types/store';
import type { Theme, ColorScheme } from '../types/store';
export interface UseThemeReturn {
/** Current theme preference ('light', 'dark', 'system') */
@@ -14,31 +14,40 @@ export interface UseThemeReturn {
resolvedTheme: 'light' | 'dark';
/** Whether the resolved theme is dark */
isDark: boolean;
/** Current color scheme ('blue', 'green', 'orange', 'purple') */
colorScheme: ColorScheme;
/** Set theme preference */
setTheme: (theme: Theme) => void;
/** Set color scheme */
setColorScheme: (scheme: ColorScheme) => void;
/** Toggle between light and dark (ignores system) */
toggleTheme: () => void;
}
/**
* Hook for managing theme state
* Hook for managing theme state with multi-color scheme support
* @returns Theme state and actions
*
* @example
* ```tsx
* const { theme, isDark, setTheme, toggleTheme } = useTheme();
* const { theme, colorScheme, isDark, setTheme, setColorScheme, toggleTheme } = useTheme();
*
* return (
* <button onClick={toggleTheme}>
* {isDark ? 'Switch to Light' : 'Switch to Dark'}
* </button>
* <div>
* <button onClick={() => setColorScheme('blue')}>Blue Theme</button>
* <button onClick={toggleTheme}>
* {isDark ? 'Switch to Light' : 'Switch to Dark'}
* </button>
* </div>
* );
* ```
*/
export function useTheme(): UseThemeReturn {
const theme = useAppStore(selectTheme);
const resolvedTheme = useAppStore(selectResolvedTheme);
const colorScheme = useAppStore((state) => state.colorScheme);
const setThemeAction = useAppStore((state) => state.setTheme);
const setColorSchemeAction = useAppStore((state) => state.setColorScheme);
const toggleThemeAction = useAppStore((state) => state.toggleTheme);
const setTheme = useCallback(
@@ -48,6 +57,13 @@ export function useTheme(): UseThemeReturn {
[setThemeAction]
);
const setColorScheme = useCallback(
(newColorScheme: ColorScheme) => {
setColorSchemeAction(newColorScheme);
},
[setColorSchemeAction]
);
const toggleTheme = useCallback(() => {
toggleThemeAction();
}, [toggleThemeAction]);
@@ -56,7 +72,9 @@ export function useTheme(): UseThemeReturn {
theme,
resolvedTheme,
isDark: resolvedTheme === 'dark',
colorScheme,
setTheme,
setColorScheme,
toggleTheme,
};
}

View File

@@ -7,6 +7,7 @@ import { useEffect, useRef, useCallback } from 'react';
import { useNotificationStore } from '@/stores';
import { useExecutionStore } from '@/stores/executionStore';
import { useFlowStore } from '@/stores';
import { useCliStreamStore } from '@/stores/cliStreamStore';
import {
OrchestratorMessageSchema,
type OrchestratorWebSocketMessage,
@@ -54,6 +55,9 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
// Flow store for node status updates on canvas
const updateNode = useFlowStore((state) => state.updateNode);
// CLI stream store for CLI output handling
const addOutput = useCliStreamStore((state) => state.addOutput);
// Handle incoming WebSocket messages
const handleMessage = useCallback(
(event: MessageEvent) => {
@@ -63,6 +67,69 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
// Store last message for debugging
setWsLastMessage(data);
// Handle CLI messages
if (data.type?.startsWith('CLI_')) {
switch (data.type) {
case 'CLI_STARTED': {
const { executionId, tool, mode, timestamp } = data.payload;
// Add system message for CLI start
addOutput(executionId, {
type: 'system',
content: `[${new Date(timestamp).toLocaleTimeString()}] CLI execution started: ${tool} (${mode || 'default'} mode)`,
timestamp: Date.now(),
});
break;
}
case 'CLI_OUTPUT': {
const { executionId, chunkType, data: outputData, unit } = data.payload;
// Handle structured output
const unitContent = unit?.content || outputData;
const unitType = unit?.type || chunkType;
// Special handling for tool_call type
let content: string;
if (unitType === 'tool_call' && typeof unitContent === 'object' && unitContent !== null) {
// Format tool_call display
content = JSON.stringify(unitContent);
} else {
content = typeof unitContent === 'string' ? unitContent : JSON.stringify(unitContent);
}
// Split by lines and add each line to store
const lines = content.split('\n');
lines.forEach((line: string) => {
// Add non-empty lines, or single line if that's all we have
if (line.trim() || lines.length === 1) {
addOutput(executionId, {
type: unitType as any,
content: line,
timestamp: Date.now(),
});
}
});
break;
}
case 'CLI_COMPLETED': {
const { executionId, success, duration } = data.payload;
const statusText = success ? 'completed successfully' : 'failed';
const durationText = duration ? ` (${duration}ms)` : '';
addOutput(executionId, {
type: 'system',
content: `[${new Date().toLocaleTimeString()}] CLI execution ${statusText}${durationText}`,
timestamp: Date.now(),
});
break;
}
}
return;
}
// Check if this is an orchestrator message
if (!data.type?.startsWith('ORCHESTRATOR_')) {
return;
@@ -138,6 +205,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet
addLog,
completeExecution,
updateNode,
addOutput,
onMessage,
]
);