feat: Add "Copy Crash Report" button for failed/partial jobs
- Generate structured markdown crash report optimized for Claude - Includes: job metadata, timeline, progress, error, logs (last 50) - Adds context and suggested investigation steps - Orange clipboard button appears for failed/partial jobs - Shows green checkmark briefly after successful copy - Fetches logs async when generating report Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,110 @@ export default function JobsView({ jobs, onSelectJob, isLoadingJob, onRefresh }:
|
||||
}
|
||||
};
|
||||
|
||||
// Generate crash report for a job
|
||||
const [copyingCrashReport, setCopyingCrashReport] = useState<string | null>(null);
|
||||
|
||||
const generateCrashReport = async (job: JobStatus): Promise<string> => {
|
||||
// Fetch logs for the job
|
||||
let logs: LogEntry[] = [];
|
||||
let logCount = 0;
|
||||
try {
|
||||
const response = await fetch(`/api/jobs/${job.job_id}/logs`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
logs = data.logs || [];
|
||||
logCount = data.log_count || 0;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch logs for crash report:', err);
|
||||
}
|
||||
|
||||
const businessName = extractBusinessName(job);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Format logs (last 50 entries)
|
||||
const recentLogs = [...logs]
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, 50)
|
||||
.reverse();
|
||||
|
||||
const logsFormatted = recentLogs.length > 0
|
||||
? recentLogs.map(log =>
|
||||
`[${new Date(log.timestamp).toISOString()}] [${log.level.toUpperCase()}] [${log.source}] ${log.message}`
|
||||
).join('\n')
|
||||
: 'No logs available';
|
||||
|
||||
// Calculate duration
|
||||
const duration = job.scrape_time
|
||||
? formatDuration(job.scrape_time)
|
||||
: job.started_at
|
||||
? formatDuration((Date.now() - new Date(job.started_at).getTime()) / 1000)
|
||||
: 'Unknown';
|
||||
|
||||
// Build the crash report
|
||||
const report = `## Crash Report: ${job.job_id}
|
||||
|
||||
**Generated**: ${now}
|
||||
**Status**: ${job.status.toUpperCase()}
|
||||
**Job Type**: google-reviews
|
||||
|
||||
### Business Info
|
||||
- **Name**: ${businessName}
|
||||
- **Address**: ${job.business_address || 'N/A'}
|
||||
- **Category**: ${job.business_category || 'N/A'}
|
||||
- **URL**: ${job.url}
|
||||
|
||||
### Job Timeline
|
||||
- **Created**: ${job.created_at}
|
||||
- **Started**: ${job.started_at || 'N/A'}
|
||||
- **Completed**: ${job.completed_at || 'N/A'}
|
||||
- **Last Update**: ${job.updated_at || 'N/A'}
|
||||
- **Duration**: ${duration}
|
||||
|
||||
### Progress at Failure
|
||||
- **Reviews Collected**: ${job.reviews_count ?? 0}${job.total_reviews ? ` / ${job.total_reviews}` : ''}
|
||||
- **Expected Total**: ${job.total_reviews_snapshot ?? 'Unknown'}
|
||||
- **Rating Snapshot**: ${job.rating_snapshot ?? 'N/A'}
|
||||
|
||||
### Error
|
||||
\`\`\`
|
||||
${job.error_message || 'No error message captured'}
|
||||
\`\`\`
|
||||
|
||||
### Logs (${logCount} total, showing last ${recentLogs.length})
|
||||
\`\`\`
|
||||
${logsFormatted}
|
||||
\`\`\`
|
||||
|
||||
### Context for Debugging
|
||||
- This is a Google Reviews scraper job
|
||||
- The scraper uses Playwright to navigate Google Maps
|
||||
- Reviews are extracted by scrolling through the reviews panel
|
||||
- Common failure points: rate limiting, DOM structure changes, network timeouts
|
||||
|
||||
### Suggested Investigation
|
||||
1. Check if error is related to rate limiting (look for 429 or "too many requests")
|
||||
2. Check if DOM selectors have changed (look for "element not found" errors)
|
||||
3. Check network/timeout issues (look for "timeout" or "navigation" errors)
|
||||
4. Review the last few log entries before the error for context
|
||||
`;
|
||||
|
||||
return report;
|
||||
};
|
||||
|
||||
const copyCrashReport = async (job: JobStatus) => {
|
||||
setCopyingCrashReport(job.job_id);
|
||||
try {
|
||||
const report = await generateCrashReport(job);
|
||||
await navigator.clipboard.writeText(report);
|
||||
// Brief visual feedback
|
||||
setTimeout(() => setCopyingCrashReport(null), 1500);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy crash report:', err);
|
||||
setCopyingCrashReport(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Live monitoring functions
|
||||
const startMonitoring = useCallback((job: JobStatus) => {
|
||||
setMonitoredJob(job);
|
||||
@@ -887,6 +991,30 @@ export default function JobsView({ jobs, onSelectJob, isLoadingJob, onRefresh }:
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Copy Crash Report - for failed or partial jobs */}
|
||||
{(job.status === 'failed' || job.status === 'partial') && (
|
||||
<button
|
||||
onClick={() => copyCrashReport(job)}
|
||||
disabled={copyingCrashReport === job.job_id}
|
||||
className={`p-1.5 rounded-lg transition-colors ${
|
||||
copyingCrashReport === job.job_id
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-orange-100 text-orange-700 hover:bg-orange-200'
|
||||
}`}
|
||||
title={copyingCrashReport === job.job_id ? 'Copied!' : 'Copy crash report for Claude'}
|
||||
>
|
||||
{copyingCrashReport === job.job_id ? (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Delete Job - allow for non-running or stuck jobs */}
|
||||
{(() => {
|
||||
const isStuck = job.status === 'running' &&
|
||||
@@ -914,7 +1042,7 @@ export default function JobsView({ jobs, onSelectJob, isLoadingJob, onRefresh }:
|
||||
},
|
||||
},
|
||||
],
|
||||
[isLoadingJob, loadingLogs, expandedErrors, jobsWithUpdates, startMonitoring, isDeleting]
|
||||
[isLoadingJob, loadingLogs, expandedErrors, jobsWithUpdates, startMonitoring, isDeleting, copyingCrashReport, copyCrashReport]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
|
||||
Reference in New Issue
Block a user