From 7e7c3c999b5fdfa10672fa3c1243609ae40c247a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 18 Jul 2026 17:04:13 -0700 Subject: [PATCH] FN-8317: add reviewed report screenshots and activity traces Capture optional report context while preserving scrubbed text-first filing.\n\n- Add browser screenshot capture, review, validation, and compensating upload cleanup.\n- Include bounded activity traces in report drafts and filing flows.\n- Preserve roadmap deduplication and document report privacy behavior.\n\nFiles changed:\n .changeset/fn-8317-report-context.md | 7 + docs/dashboard-guide.md | 4 +- packages/dashboard/app/api/report.ts | 15 +- packages/dashboard/app/components/ReportModal.css | 5 +- packages/dashboard/app/components/ReportModal.tsx | 52 +++---- .../app/components/__tests__/ReportModal.test.tsx | 14 ++ packages/dashboard/app/hooks/useViewState.ts | 7 + .../app/utils/__tests__/report-capture.test.ts | 17 +++ packages/dashboard/app/utils/report-capture.ts | 47 +++++++ packages/dashboard/package.json | 1 - .../src/__tests__/report-pipeline.test.ts | 100 +++++++++++++- .../dashboard/src/__tests__/report-routes.test.ts | 17 +++ .../dashboard/src/__tests__/report-scrub.test.ts | 8 ++ packages/dashboard/src/github.ts | 107 +++++++++++++++ packages/dashboard/src/report-pipeline.ts | 152 ++++++++++++++++----- packages/dashboard/src/report-scrub.ts | 8 ++ .../dashboard/src/routes/register-report-routes.ts | 103 ++++++-------- pnpm-lock.yaml | 129 +++++++++-------- 18 files changed, 590 insertions(+), 203 deletions(-) Fusion-Task-Id: FN-8317 Fusion-Task-Lineage: 9d200cee-cf69-4827-9e1e-f7789adf09e0 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8317-report-context.md | 7 + docs/dashboard-guide.md | 4 +- packages/dashboard/app/api/report.ts | 15 +- .../dashboard/app/components/ReportModal.css | 5 +- .../dashboard/app/components/ReportModal.tsx | 52 +++--- .../components/__tests__/ReportModal.test.tsx | 14 ++ packages/dashboard/app/hooks/useViewState.ts | 7 + .../utils/__tests__/report-capture.test.ts | 17 ++ .../dashboard/app/utils/report-capture.ts | 47 ++++++ packages/dashboard/package.json | 1 - .../src/__tests__/report-pipeline.test.ts | 100 +++++++++++- .../src/__tests__/report-routes.test.ts | 17 ++ .../src/__tests__/report-scrub.test.ts | 8 + packages/dashboard/src/github.ts | 107 ++++++++++++ packages/dashboard/src/report-pipeline.ts | 152 +++++++++++++----- packages/dashboard/src/report-scrub.ts | 8 + .../src/routes/register-report-routes.ts | 103 +++++------- pnpm-lock.yaml | 129 ++++++++------- 18 files changed, 590 insertions(+), 203 deletions(-) create mode 100644 .changeset/fn-8317-report-context.md create mode 100644 packages/dashboard/app/utils/__tests__/report-capture.test.ts create mode 100644 packages/dashboard/app/utils/report-capture.ts diff --git a/.changeset/fn-8317-report-context.md b/.changeset/fn-8317-report-context.md new file mode 100644 index 0000000000..66dcaf0001 --- /dev/null +++ b/.changeset/fn-8317-report-context.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add opt-in reviewed screenshots and scrubbed activity traces to in-app reports. +category: feature +dev: Uses native Screen Capture API; unavailable screenshot hosting falls back to text-only filing. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 0e0af098a9..7a7c7f7c66 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -2094,4 +2094,6 @@ Projects can opt into **Check roadmap before filing reports** in **Settings → In **Settings → General**, choose **Review draft before filing** (the default) or **File automatically**. Both paths show the resulting issue, Discussion, or endorsement link. Help checks Fusion's local knowledge index on every server report path first and only escalates when it cannot find an answer. -Reports include a default-on activity trace of up to 20 recent views, report actions, toast messages, and uncaught client errors (at most 4,000 characters). It is ordinary report text and receives the same server-side scrub as every other report field. You can optionally attach a screenshot of the current Fusion view. Fusion captures only the dashboard DOM, shows the exact preview, and requires confirmation before it uploads the PNG/JPEG (up to 2MB) as a local image artifact. In automatic mode this confirmation and local artifact upload happen before filing begins. The server validates the resulting artifact reference (format, image type, MIME type, and report-upload provenance) before it can appear as a text-only local-retention note in a report. Screenshot pixels are never sent to GitHub. +Reports can include a short activity trace of recent built-in view names (up to 20 entries). The trace is ordinary text and receives the same mandatory server-side scrub as every other report field on every egress path, including edited drafts and duplicate endorsements. + +Choose **Attach a screenshot** to request the browser's screen-capture permission and capture one PNG frame. The modal shows the image for review and lets you remove it before continuing. Screenshot pixels are binary and cannot be text-scrubbed, so Fusion never captures or files one automatically: it is included only after this explicit per-report choice, including in automatic filing mode. Fusion first validates and files the scrubbed text report, then hosts and posts the reviewed image as a follow-up only when an approved GitHub image host is available. If that follow-up fails after hosting, Fusion compensates by deleting the uploaded image; it never inserts an unhosted data URL into report text. diff --git a/packages/dashboard/app/api/report.ts b/packages/dashboard/app/api/report.ts index ae9dfe3d78..6936e198ac 100644 --- a/packages/dashboard/app/api/report.ts +++ b/packages/dashboard/app/api/report.ts @@ -6,18 +6,9 @@ async function post(path: string, body: unknown) { return response.json(); } -export interface ReportActivityTraceEntry { ts: string; kind: string; label: string; } -export interface ReportContextInput { actionType: ReportActionType; userPrompt: string; contextRefs?: { taskId?: string; agentId?: string }; activityTrace?: ReportActivityTraceEntry[]; screenshotArtifactId?: string; } - -export async function uploadReportScreenshot(blob: Blob, contextRefs?: { taskId?: string; agentId?: string }): Promise<{ artifactId: string; uri?: string }> { - const form = new FormData(); - form.append("screenshot", blob, "report-screenshot.jpg"); - if (contextRefs) form.append("contextRefs", JSON.stringify(contextRefs)); - const response = await fetch("/api/report/attachment", { method: "POST", body: form }); - if (!response.ok) throw new Error((await response.json().catch(() => ({ error: response.statusText }))).error ?? response.statusText); - return response.json(); -} +export interface ReportScreenshot { dataUrl: string; capturedAt: string; } +export interface ReportContextInput { actionType: ReportActionType; userPrompt: string; contextRefs?: { taskId?: string; agentId?: string }; activityTrace?: string[]; screenshot?: ReportScreenshot; } export function reportDraft(input: ReportContextInput) { return post("/api/report/draft", input); } -export function reportFile(input: { actionType: ReportActionType; report: unknown; endorseIssueNumber?: number; endorseDiscussionId?: string; activityTrace?: ReportActivityTraceEntry[]; screenshotArtifactId?: string }) { return post("/api/report/file", input); } +export function reportFile(input: { actionType: ReportActionType; report: unknown; endorseIssueNumber?: number; endorseDiscussionId?: string; activityTrace?: string[]; screenshot?: ReportScreenshot }) { return post("/api/report/file", input); } export function reportHelp(question: string) { return post("/api/report/help", { question }); } diff --git a/packages/dashboard/app/components/ReportModal.css b/packages/dashboard/app/components/ReportModal.css index 9fbca59af6..5a5675134d 100644 --- a/packages/dashboard/app/components/ReportModal.css +++ b/packages/dashboard/app/components/ReportModal.css @@ -3,7 +3,10 @@ .report-modal textarea { min-block-size: var(--space-32); resize: vertical; } .report-modal__close { position: absolute; inset-block-start: var(--space-2); inset-inline-end: var(--space-2); } .report-modal__error { color: var(--color-error); } +.report-modal__warning { color: var(--color-warning); } .report-modal__screenshot-option { display: flex; gap: var(--space-2); align-items: center; } .report-modal__screenshot-preview { display: grid; gap: var(--space-2); padding: var(--space-3); border-radius: var(--radius-md); background: color-mix(in srgb, var(--bg-raised) 85%, transparent); } .report-modal__screenshot-preview img { max-inline-size: 100%; max-block-size: var(--space-96); object-fit: contain; } -@media (max-width: 768px) { .report-modal { inline-size: 100%; padding: var(--space-4); } } +.report-modal__activity-trace { padding: var(--space-3); border-radius: var(--radius-md); background: color-mix(in srgb, var(--bg-raised) 85%, transparent); } +.report-modal__activity-trace ul { margin: var(--space-2) 0 0; padding-inline-start: var(--space-5); } +@media (max-width: 768px) { .report-modal { inline-size: 100%; padding: var(--space-4); } .report-modal__screenshot-preview img { max-block-size: var(--space-64); } } diff --git a/packages/dashboard/app/components/ReportModal.tsx b/packages/dashboard/app/components/ReportModal.tsx index f4fc315f2f..29f8e4e9b7 100644 --- a/packages/dashboard/app/components/ReportModal.tsx +++ b/packages/dashboard/app/components/ReportModal.tsx @@ -1,13 +1,15 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; + import type { ReportActionType } from "@fusion/core"; -import { reportDraft, reportFile, reportHelp, uploadReportScreenshot } from "../api"; -import { captureAppScreenshot, type CapturedScreenshot } from "../utils/capture-screenshot"; -import { recordActivity, snapshotActivityTrace } from "../utils/activity-trace"; +import { reportDraft, reportFile, reportHelp } from "../api"; +import { captureScreenshot as captureScreen, getRecentActivity, recordActivity, type ReportScreenshot } from "../utils/report-capture"; import "./ReportModal.css"; const prompts: Record = { bug: "What went wrong?", feedback: "What would you like to share?", idea: "What would you like Fusion to do?", help: "What would you like help with?" }; -type ModalResult = { kind: string; report?: { userPrompt: string; sourcePrompt?: string; summary?: string; body?: string; context?: Record; sessionToken?: string }; issue?: { number: number; url: string; title: string; discussionId?: string }; roadmap?: { featureId: string; title: string; description: string }; url?: string; answer?: { summary?: string; content?: string }; message?: string }; + +type ModalResult = { kind: string; report?: { userPrompt: string; sourcePrompt?: string; summary?: string; body?: string; context?: Record; sessionToken?: string }; issue?: { number: number; url: string; title: string; discussionId?: string }; roadmap?: { featureId: string; title: string; description: string }; url?: string; answer?: { summary?: string; content?: string }; message?: string; screenshotNotAttached?: boolean }; + /** * FNXC:ReportPipeline 2026-07-16-12:00: @@ -21,17 +23,14 @@ export function ReportModal({ actionType, onClose, contextRefs }: { actionType: const [busy, setBusy] = useState(false); const [error, setError] = useState(); const [screenshotEnabled, setScreenshotEnabled] = useState(false); - const [capturedScreenshot, setCapturedScreenshot] = useState(); - const [screenshotConfirmed, setScreenshotConfirmed] = useState(false); - const [screenshotArtifactId, setScreenshotArtifactId] = useState(); - useEffect(() => () => { if (capturedScreenshot) URL.revokeObjectURL(capturedScreenshot.previewUrl); }, [capturedScreenshot]); + const [capturedScreenshot, setCapturedScreenshot] = useState(); const captureScreenshot = async () => { setBusy(true); setError(undefined); try { - const captured = await captureAppScreenshot(); + const captured = await captureScreen(); + if (!captured) throw new Error("Screen capture was unavailable or denied."); setCapturedScreenshot(captured); - setScreenshotConfirmed(false); } catch (captureError) { setScreenshotEnabled(false); setError(captureError instanceof Error ? captureError.message : "We could not capture the current screen."); @@ -39,24 +38,19 @@ export function ReportModal({ actionType, onClose, contextRefs }: { actionType: }; const submit = async () => { if (!prompt.trim()) return; - if (screenshotEnabled && (!capturedScreenshot || !screenshotConfirmed)) { - setError("Preview and confirm the screenshot before continuing."); + if (screenshotEnabled && !capturedScreenshot) { + setError("Capture a screenshot before continuing, or turn attachment off."); return; } setBusy(true); setError(undefined); try { - recordActivity({ kind: "report", label: `${actionType} report submitted` }); + recordActivity("report"); if (actionType === "help") { const help = await reportHelp(prompt); if (help.answered) { setResult({ kind: "help", answer: help.answer }); return; } } - const attachment = screenshotEnabled && capturedScreenshot && !screenshotArtifactId - ? await uploadReportScreenshot(capturedScreenshot.blob, contextRefs) - : undefined; - const artifactId = attachment?.artifactId ?? screenshotArtifactId; - if (artifactId) setScreenshotArtifactId(artifactId); - setResult(await reportDraft({ actionType, userPrompt: prompt, contextRefs, activityTrace: snapshotActivityTrace(), screenshotArtifactId: artifactId })); + setResult(await reportDraft({ actionType, userPrompt: prompt, contextRefs, activityTrace: getRecentActivity(), screenshot: screenshotEnabled ? capturedScreenshot : undefined })); } catch { setError("We could not prepare your report. Check your connection and try again."); } finally { setBusy(false); } @@ -66,8 +60,9 @@ export function ReportModal({ actionType, onClose, contextRefs }: { actionType: setBusy(true); setError(undefined); try { - recordActivity({ kind: "report", label: `${actionType} report filed` }); - setResult(await reportFile({ actionType, report: result.report, endorseIssueNumber, endorseDiscussionId, activityTrace: snapshotActivityTrace(), screenshotArtifactId })); + recordActivity("report"); +setResult(await reportFile({ actionType, report: result.report, endorseIssueNumber, endorseDiscussionId, activityTrace: getRecentActivity(), screenshot: screenshotEnabled ? capturedScreenshot : undefined })); + } catch { setError("We could not send your report. Your draft is still here; try again."); } finally { setBusy(false); } @@ -76,11 +71,14 @@ export function ReportModal({ actionType, onClose, contextRefs }: { actionType: {error &&

{error}

} {!result && <>

{actionType[0].toUpperCase() + actionType.slice(1)}