Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 34 additions & 153 deletions components/BugReportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,168 +1,49 @@
import { useEffect, useRef, useState } from 'react';
import { APP_VERSION } from '@/lib/version';

interface BugReportModalProps {
debugOutput: string;
onClose: () => void;
}

interface TurnstileApi {
render: (container: HTMLElement, options: {
sitekey: string;
callback: (token: string) => void;
'expired-callback': () => void;
'error-callback': () => void;
}) => string;
remove: (widgetId: string) => void;
}

declare global {
interface Window {
turnstile?: TurnstileApi;
}
}

const TURNSTILE_SCRIPT = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
const TEST_SITE_KEY = '1x00000000000000000000AA';

export default function BugReportModal({ debugOutput, onClose }: BugReportModalProps) {
const [happened, setHappened] = useState('');
const [expected, setExpected] = useState('');
const [steps, setSteps] = useState('');
const [email, setEmail] = useState('');
const [includeDebugOutput, setIncludeDebugOutput] = useState(false);
const [turnstileToken, setTurnstileToken] = useState('');
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const turnstileRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const sitekey = import.meta.env.VITE_TURNSTILE_SITE_KEY || (import.meta.env.DEV ? TEST_SITE_KEY : '');
if (!sitekey || !turnstileRef.current) return;

let widgetId: string | null = null;
let cancelled = false;

const renderWidget = () => {
if (cancelled || !window.turnstile || !turnstileRef.current || widgetId) return;
widgetId = window.turnstile.render(turnstileRef.current, {
sitekey,
callback: setTurnstileToken,
'expired-callback': () => setTurnstileToken(''),
'error-callback': () => setTurnstileToken(''),
});
};

const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${TURNSTILE_SCRIPT}"]`);
if (existingScript) {
if (window.turnstile) renderWidget();
else existingScript.addEventListener('load', renderWidget, { once: true });
} else {
const script = document.createElement('script');
script.src = TURNSTILE_SCRIPT;
script.async = true;
script.defer = true;
script.addEventListener('load', renderWidget, { once: true });
document.head.appendChild(script);
}

return () => {
cancelled = true;
if (widgetId && window.turnstile) window.turnstile.remove(widgetId);
};
}, []);

const submitReport = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setStatus('sending');
setErrorMessage('');

try {
const response = await fetch('/api/report-bug', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
happened,
expected,
steps,
email,
debugOutput: includeDebugOutput ? debugOutput : '',
turnstileToken,
environment: {
appVersion: APP_VERSION,
page: window.location.href,
userAgent: navigator.userAgent,
},
}),
});

const responseBody = await response.text();
let result: { error?: string } = {};
if (responseBody) {
try {
result = JSON.parse(responseBody) as { error?: string };
} catch {
// Some hosting and proxy errors return HTML or plain text. The HTTP
// status below is more useful to the reporter than a JSON parse error.
}
}

if (!response.ok) {
const localDevelopmentError = import.meta.env.DEV && response.status === 404
? 'Bug reports cannot be sent from the local Vite server because the Cloudflare reporting endpoint is not running. Submit the report from the deployed BRIGX site.'
: '';
throw new Error(result.error || localDevelopmentError || `The report could not be sent (HTTP ${response.status}).`);
}
setStatus('sent');
} catch (error) {
setStatus('error');
setErrorMessage(error instanceof Error ? error.message : 'The report could not be sent.');
}
};
const REPORT_EMAIL = 'nabil@happykhan.com';

export default function BugReportModal({ onClose }: BugReportModalProps) {
const reportTemplate = [
'Hello,',
'',
'What happened?',
'',
'What did you expect?',
'',
'Steps to reproduce:',
'',
`BRIGX page: ${window.location.href}`,
'Browser and operating system:',
'Relevant error message:',
].join('\n');
const emailHref = `mailto:${REPORT_EMAIL}?subject=${encodeURIComponent('BRIGX bug report')}&body=${encodeURIComponent(reportTemplate)}`;

return (
<div className="fixed inset-0 flex items-center justify-center z-50 p-4" style={{ background: 'rgba(0, 0, 0, 0.6)' }} onClick={onClose}>
<div role="dialog" aria-modal="true" aria-labelledby="bug-report-title" className="rounded-lg max-w-lg w-full p-6 max-h-[90vh] overflow-y-auto" style={{ background: 'var(--gx-bg-alt)' }} onClick={event => event.stopPropagation()}>
<div role="dialog" aria-modal="true" aria-labelledby="bug-report-title" className="rounded-lg max-w-lg w-full p-6" style={{ background: 'var(--gx-bg-alt)' }} onClick={event => event.stopPropagation()}>
<div className="flex justify-between items-center mb-4">
<h2 id="bug-report-title" className="text-xl font-semibold">Report a bug</h2>
<button type="button" onClick={onClose} aria-label="Close bug report">×</button>
</div>

{status === 'sent' ? (
<div>
<p>Thanks. Your report has been sent.</p>
<button type="button" className="btn-primary mt-5" onClick={onClose}>Close</button>
</div>
) : (
<form className="space-y-4" onSubmit={submitReport}>
<label className="block">
<span className="block text-sm font-medium mb-1">What happened?</span>
<textarea required maxLength={4000} rows={4} className="input-field w-full" value={happened} onChange={event => setHappened(event.target.value)} />
</label>
<label className="block">
<span className="block text-sm font-medium mb-1">What did you expect?</span>
<textarea required maxLength={2000} rows={3} className="input-field w-full" value={expected} onChange={event => setExpected(event.target.value)} />
</label>
<label className="block">
<span className="block text-sm font-medium mb-1">Steps to reproduce</span>
<textarea required maxLength={4000} rows={4} className="input-field w-full" value={steps} onChange={event => setSteps(event.target.value)} />
</label>
<label className="block">
<span className="block text-sm font-medium mb-1">Email address <span className="font-normal" style={{ color: 'var(--gx-text-muted)' }}>(optional)</span></span>
<input type="email" maxLength={254} className="input-field w-full" value={email} onChange={event => setEmail(event.target.value)} />
</label>
<label className="flex items-start gap-2 text-sm">
<input type="checkbox" checked={includeDebugOutput} onChange={event => setIncludeDebugOutput(event.target.checked)} />
Include the BRIGX debug output with this report
</label>
<p className="text-xs" style={{ color: 'var(--gx-text-muted)' }}>Do not include confidential, patient-identifiable, or raw sequence data.</p>
<div ref={turnstileRef} />
{status === 'error' && <p role="alert" style={{ color: 'var(--gx-error)' }}>{errorMessage}</p>}
<button type="submit" className="btn-primary" disabled={status === 'sending' || !turnstileToken}>
{status === 'sending' ? 'Sending…' : 'Send report'}
</button>
</form>
)}
<p>
Email <a className="underline" href={`mailto:${REPORT_EMAIL}`}>{REPORT_EMAIL}</a> and include:
</p>
<ul className="list-disc pl-6 mt-3 space-y-2">
<li>what happened and what you expected;</li>
<li>the steps needed to reproduce the problem;</li>
<li>the error message and a screenshot, if available;</li>
<li>your browser and operating system.</li>
</ul>
<p className="text-sm mt-4" style={{ color: 'var(--gx-text-muted)' }}>
Do not include confidential, patient-identifiable, or raw sequence data.
</p>
<div className="flex justify-end gap-3 mt-6">
<button type="button" className="btn-secondary" onClick={onClose}>Close</button>
<a className="btn-primary" href={emailHref}>Email bug report</a>
</div>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Confirm that the feature file and loaded reference use the same assembly and coo

## Alignment workers fail to start

Open the debug console and retain the complete error. Reload once to exclude a stale browser asset, then use **Report a bug** and include the debug output, browser version and whether the failure occurs with the bundled example.
Open the debug console and retain the complete error. Reload once to exclude a stale browser asset, then use **Report a bug** to email nabil@happykhan.com. Include the error message, steps to reproduce, browser and operating system, and whether the failure occurs with the bundled example. Do not send confidential, patient-identifiable, or raw sequence data.

## A public session URL does not load

Expand Down
128 changes: 0 additions & 128 deletions functions/api/report-bug.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ export default function Home() {

{bugReportOpen && (
<BugReportModal
debugOutput={consoleLogs.join('\n')}
onClose={() => setBugReportOpen(false)}
/>
)}
Expand Down
11 changes: 9 additions & 2 deletions tests/e2e/brigx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,16 @@ test.describe('BRIGX e2e — circular genome plot', () => {
await page.goto('/app')
await page.getByRole('button', { name: 'Report a bug' }).click()
await expect(page.getByRole('dialog', { name: 'Report a bug' })).toBeVisible()
await expect(page.getByLabel('What happened?')).toBeVisible()
await expect(page.getByLabel(/Email address/)).toBeVisible()
await expect(page.getByRole('link', { name: 'nabil@happykhan.com' })).toHaveAttribute('href', 'mailto:nabil@happykhan.com')
const emailReportLink = page.getByRole('link', { name: 'Email bug report' })
await expect(emailReportLink).toBeVisible()
await expect(emailReportLink).toHaveAttribute('href', /^mailto:nabil@happykhan\.com\?subject=BRIGX%20bug%20report&body=/)
await expect(page.getByText(/what happened and what you expected/)).toBeVisible()
await expect(page.getByText(/steps needed to reproduce/)).toBeVisible()
await expect(page.getByText(/error message and a screenshot/)).toBeVisible()
await expect(page.getByText(/browser and operating system/)).toBeVisible()
await expect(page.getByText(/Do not include confidential/)).toBeVisible()
await expect(page.getByRole('button', { name: 'Send report' })).toHaveCount(0)
})

test('uploading a reference FASTA shows the filename', async ({ page }) => {
Expand Down
Loading