agent-ecosystem/src/renderer/utils/bugReportUtils.ts
iliya b6ec408451 feat: enhance error handling and reporting in ErrorBoundary component
- Added functionality to copy error details to clipboard and create GitHub issue reports directly from the error boundary.
- Introduced a new state variable to manage the copy confirmation status.
- Enhanced UI with buttons for copying error details and reporting bugs, improving user experience during error handling.
- Updated the rendering logic to display additional context about the error and the copied status.
- Refactored the component to ensure proper cleanup of timeouts on unmount.
2026-03-11 13:28:44 +02:00

157 lines
4.8 KiB
TypeScript

import packageJson from '../../../package.json';
const GITHUB_BUG_REPORT_URL = 'https://github.com/777genius/claude_agent_teams_ui/issues/new';
const MAX_TITLE_LENGTH = 120;
const URL_MAX_STACK_LENGTH = 1800;
const URL_MAX_COMPONENT_STACK_LENGTH = 1200;
const COPY_MAX_STACK_LENGTH = 12000;
const COPY_MAX_COMPONENT_STACK_LENGTH = 8000;
export interface BugReportContext {
activeTabType?: string | null;
activeTabLabel?: string | null;
activeTeamName?: string | null;
selectedTeamName?: string | null;
taskId?: string | null;
sessionId?: string | null;
projectId?: string | null;
}
export interface BugReportOptions {
error: Error | null;
componentStack?: string | null;
context?: BugReportContext;
}
const truncate = (value: string, maxLength: number): string => {
if (value.length <= maxLength) {
return value;
}
return `${value.slice(0, maxLength)}\n...[truncated]`;
};
const buildIssueTitle = (error: Error | null): string => {
const baseTitle = error ? `[BUG] ${error.name}: ${error.message}` : '[BUG] Application crash';
return truncate(baseTitle, MAX_TITLE_LENGTH);
};
const getRuntimeLabel = (): string => (window.electronAPI ? 'Electron renderer' : 'Web browser');
const formatOptional = (value: string | null | undefined): string => {
if (!value) {
return 'Not available';
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : 'Not available';
};
const getOperatingSystemLabel = (): string => {
const { userAgent } = window.navigator;
if (userAgent.includes('Mac OS X')) return 'macOS';
if (userAgent.includes('Windows')) return 'Windows';
if (userAgent.includes('Linux')) return 'Linux';
return 'Unknown';
};
const formatActiveTab = (context?: BugReportContext): string => {
if (!context?.activeTabType) {
return 'Not available';
}
if (!context.activeTabLabel) {
return context.activeTabType;
}
return `${context.activeTabType} (${context.activeTabLabel})`;
};
const buildBugReportMarkdown = (
{ error, componentStack, context }: BugReportOptions,
stackLimits: { js: number; react: number }
): string => {
const message = error?.message ?? 'Unknown application crash';
const jsStack = error?.stack ? truncate(error.stack, stackLimits.js) : 'Not available';
const reactComponentStack = componentStack
? truncate(componentStack, stackLimits.react)
: 'Not available';
return [
'**Describe the bug**',
'The app crashed and showed the global error screen.',
'',
'**What happened**',
`- Error: \`${message}\``,
`- Error type: \`${error?.name ?? 'UnknownError'}\``,
`- Active tab: ${formatActiveTab(context)}`,
`- Active team tab: ${formatOptional(context?.activeTeamName)}`,
`- Selected team: ${formatOptional(context?.selectedTeamName)}`,
`- Current task: ${formatOptional(context?.taskId)}`,
`- Session ID: ${formatOptional(context?.sessionId)}`,
`- Project ID: ${formatOptional(context?.projectId)}`,
'',
'**Steps to reproduce**',
'1. Open the app and navigate to the screen where the crash happened.',
'2. Repeat the action that triggered the error.',
'3. Observe the global error screen.',
'',
'**Expected behavior**',
'The app should continue working instead of crashing.',
'',
'**Screenshots**',
'Attach a screenshot if you have one.',
'',
'**Environment**',
`- OS: ${getOperatingSystemLabel()}`,
`- Runtime: ${getRuntimeLabel()}`,
`- App version: ${packageJson.version}`,
'',
'**Diagnostics**',
'```text',
`Timestamp: ${new Date().toISOString()}`,
`Current URL: ${window.location.href}`,
`User Agent: ${window.navigator.userAgent}`,
`Error name: ${error?.name ?? 'UnknownError'}`,
`Error message: ${message}`,
`Active tab: ${formatActiveTab(context)}`,
`Active team tab: ${formatOptional(context?.activeTeamName)}`,
`Selected team: ${formatOptional(context?.selectedTeamName)}`,
`Current task: ${formatOptional(context?.taskId)}`,
`Session ID: ${formatOptional(context?.sessionId)}`,
`Project ID: ${formatOptional(context?.projectId)}`,
'```',
'',
'**JavaScript stack trace**',
'```text',
jsStack,
'```',
'',
'**React component stack**',
'```text',
reactComponentStack,
'```',
].join('\n');
};
export const buildBugReportText = (options: BugReportOptions): string =>
buildBugReportMarkdown(options, {
js: COPY_MAX_STACK_LENGTH,
react: COPY_MAX_COMPONENT_STACK_LENGTH,
});
export const buildGitHubBugReportUrl = (options: BugReportOptions): string => {
const params = new URLSearchParams({
template: 'bug_report.md',
labels: 'bug',
title: buildIssueTitle(options.error),
body: buildBugReportMarkdown(options, {
js: URL_MAX_STACK_LENGTH,
react: URL_MAX_COMPONENT_STACK_LENGTH,
}),
});
return `${GITHUB_BUG_REPORT_URL}?${params.toString()}`;
};