spaps-issue-reporting-react
v0.6.4
Published
Shared React issue-reporting UI for Sweet Potato platform consumers
Maintainers
Readme
spaps-issue-reporting-react
Shared React issue-reporting UI for SPAPS-compatible apps.
Examples in this README use placeholder IDs and app labels. Replace them with values from your own product, session model, and API client.
Install
npm install spaps-issue-reporting-react react react-dom @tanstack/react-queryThis package targets Node.js >=18 and React 18+. Voice input uses the package's @elevenlabs/react dependency and a SPAPS-minted single-use token.
When It Fits
| Need | Package gives you |
| --- | --- |
| A visible issue-report entry point | Floating button with open/recent state |
| A guided reporting flow | Page-first create flow, optional section picking, create/edit/reply modal |
| Screenshot evidence | Optional picker and opt-in lazy DOM capture that upload private PNG/JPEG/WebP attachments through the host client |
| A lightweight integration contract | Any client that exposes issueReporting.* methods |
| App-level control | Eligibility, reporter identity, scope, page policy, and copy stay in your app |
Quick Start
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
FloatingIssueReportButton,
IssueReportingPageConfig,
IssueReportingProvider,
ReportableSection,
} from "spaps-issue-reporting-react";
import { spaps } from "./spapsClient";
const queryClient = new QueryClient();
export function AppShell() {
return (
<QueryClientProvider client={queryClient}>
<IssueReportingProvider
client={spaps}
isEligible={true}
principalId="user_123"
reporterRoleHint="staff"
defaultScope="mine"
inputModes={["text", "voice"]}
defaultInputMode="text"
>
<IssueReportingPageConfig createMode="surface_preferred" />
<ReportableSection
reportableName={{
componentKey: "patient_chart",
componentLabel: "Patient Chart",
metadata: { area: "overview" },
}}
className="rounded-xl"
>
<PatientChart />
</ReportableSection>
<FloatingIssueReportButton />
</IssueReportingProvider>
</QueryClientProvider>
);
}What Your App Still Owns
- A client with
issueReporting.getStatus,list,get,create,update, andreply. - A client with
issueReporting.createVoiceTokenwheninputModesincludesvoice. - A client with
issueReporting.uploadAttachment(file, { filename })when you want screenshot uploads. - For automatic screenshot capture,
spaps-sdk >=1.10.1or an equivalent client that implementsissueReporting.uploadAttachment(file, { filename }). - Optionally, a client with
issueReporting.listMessages(issueReportId)andissueReporting.submitMessage(issueReportId, { body, idempotency_key })to wire the operator-to-reporter message thread (and itsneeds_responsebadge). When these are present, the bundledIssueReportMessageThreadrenders the conversation inside the issue detail modal (see Conversation Thread). - Any auth and token refresh behavior needed by that client.
- Eligibility rules such as feature flags, account state, or role checks.
- The current principal ID and optional role hint passed into the provider.
- Whether users can switch between
mineand a custom tenant queue scope. The stock SPAPS API supportsmineonly; useallowTenantScopeonly with a client that implements tenant reads. - Whether a page defaults to
general_page,surface_required, orsurface_preferredreporting. - Whether the app allows
["text"],["voice"], or["text", "voice"]report input. - User-facing screenshot sensitivity warnings for surfaces that may capture private data.
- Styling integration if your build strips package utility classes.
- Any app-specific copy overrides.
- Origin registration on the owning SPAPS application if the browser calls SPAPS directly with a publishable key.
For direct browser integrations, allowed_origins is stored on the SPAPS applications row that owns the publishable key. It is not configured on this package. If multiple hostnames share one SPAPS application, put all of them on that row. Updating allowed_origins does not require restarting SPAPS.
Voice Input
Voice mode is opt-in per app. Configure the owning SPAPS application with either:
{
"issue_reporting_input_modes": ["text", "voice"]
}or:
{
"issue_reporting": {
"input_modes": ["voice"]
}
}The SPAPS server also needs ELEVENLABS_API_KEY. The browser calls your normal SPAPS client, which mints a short-lived Scribe token through issueReporting.createVoiceToken(). The committed transcript is submitted as the issue note; raw audio is not stored by this package.
<IssueReportingProvider
client={spaps}
isEligible={true}
inputModes={["voice"]}
defaultInputMode="voice"
voice={{
modelId: "scribe_v2_realtime",
microphone: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
}}
>
<FloatingIssueReportButton />
</IssueReportingProvider>When text and voice are both enabled, the modal keeps the textarea available and lets the user append a committed transcript into the editable note.
Screenshot Attachments
Screenshot attachments are opt-in through the client contract. If the client passed to IssueReportingProvider exposes issueReporting.uploadAttachment, the modal renders a screenshot picker. If the method is absent, the picker stays hidden and text/voice reporting works unchanged.
const client = {
issueReporting: {
...spaps.issueReporting,
uploadAttachment: (file: Blob, options?: { filename?: string }) =>
spaps.issueReporting.uploadAttachment(file, options),
},
};The picker accepts PNG, JPEG, and WebP files, enforces the 10 MiB per-file limit and 5-screenshot report limit, previews pending files, and preserves existing attachments while editing. On submit, it uploads pending files first and sends only attachment IDs through attachment_ids, add_attachment_ids, or remove_attachment_ids.
This package does not store screenshots, generate public URLs, or perform pixel/OCR redaction after capture. SPAPS stores screenshots through its shared hosted-asset boundary as private objects, and access URLs are minted by the backend after authorization. The automatic capture path masks common sensitive controls and token-like text in the cloned DOM before rendering, but host apps should still warn users before upload when a screenshot might include PHI, credentials, payment data, or other sensitive content.
Automatic Screenshot Capture
Automatic DOM capture is opt-in and lazy. Apps that omit screenshotCapture or set enabled: false get no behavioral change and do not download html2canvas.
Peer dependency: install html2canvas (>=1.4.0) as an optional peer in the consuming app. The capture module only loads via dynamic import() when a report is submitted with capture enabled.
npm install html2canvas<IssueReportingProvider
client={spaps}
isEligible={true}
screenshotCapture={{
enabled: true,
captureOn: "before_submit",
scope: "visible_viewport",
imageType: "image/png",
excludeSelectors: [
"[data-spaps-screenshot-exclude]",
"[data-spaps-private]",
],
failureBehavior: "continue_without_screenshot",
onCaptureError: (err) => console.warn("Screenshot capture failed:", err),
}}
>
<FloatingIssueReportButton />
</IssueReportingProvider>screenshotCapture maps to IssueReportingScreenshotCaptureConfig in src/types.ts:
| Field | Behavior |
| --- | --- |
| enabled | Required opt-in. false or an absent prop means no DOM capture, no lazy import, and no behavior change. |
| captureOn | Only "before_submit" is supported: capture after the reporter submits and before pending attachments upload. |
| scope | "visible_viewport" by default. "selected_reportable_target" records the selected target in capture context; to narrow pixels to that section, pass rootElement or a getter that returns the selected same-origin element. |
| imageType | Defaults to "image/png"; must stay within SPAPS-supported PNG/JPEG/WebP uploads. |
| filename | Optional static name or callback (context) => string. The upload flows through issueReporting.uploadAttachment(file, { filename }). |
| rootElement | Optional same-origin element or getter for host shells that need to exclude app chrome or capture a selected section. If omitted, capture starts from document.documentElement and is cropped to the visible viewport. |
| rect | Optional visible-viewport crop rectangle or getter. Custom consumers can use this for click-and-drag/refarea tools while still sharing the package capture implementation. |
| excludeSelectors | Optional extra selectors to omit. The implementation always honors [data-spaps-screenshot-exclude] and [data-spaps-private] as privacy markers. Invalid custom selectors are ignored so they cannot block submission. |
| maskSelectors | Optional selectors to mask in the cloned DOM before rendering. Defaults mask form controls and editable regions. |
| pixelRatio | Optional render scale, clamped to 1..2. Defaults to window.devicePixelRatio within that range. |
| quality | Optional image quality passed to canvas encoding for lossy formats. |
| maxBytes | Cannot exceed the SPAPS 10 MiB attachment limit. An automatic screenshot counts toward the existing five attachment cap. |
| failureBehavior | Only "continue_without_screenshot" is supported. Capture/render/upload failure does not block issue submission. |
| onCaptureError | Optional host callback for telemetry or a local warning when capture fails. Receives the raw browser error; raw error text is not stored in issue metadata by this package. |
Minimum SDK capability: spaps-sdk >=1.10.1 or an equivalent client implementing issueReporting.uploadAttachment(file, options) returning IssueReportAttachmentOut.
What is captured:
- Automatic capture is opt-in and lazy. It never runs on provider mount, page load, history view, edit, reply, or when the upload helper is absent.
- The capture module dynamically imports
html2canvasonly during create submit afterscreenshotCapture.enabled === true. - Capture is cropped to the current visible viewport dimensions. It does not capture hidden browser chrome, full-page scrollback, background tabs, or data outside the visible page.
html2canvasrenders same-origin DOM it can access. Cross-origin iframes and tainted canvases are not a supported capture target.
Custom consumers that do not use the shared modal can import the same primitive without reaching into private build output:
import { captureScreenshot } from "spaps-issue-reporting-react/screenshot-capture";The subpath also exports maskScreenshotCaptureDocument, SCREENSHOT_CAPTURE_REDACTION_TEXT, and the related screenshot capture types for app-local adapters.
Sensitive-region exclusion:
- Mark sensitive DOM with
data-spaps-screenshot-excludeordata-spaps-private. - Add host-specific
excludeSelectorsfor reusable sensitive regions such as account numbers, tokens, patient identifiers, or payment fields. - Exclusion is omission during rendering, not image-content redaction after capture. The capture path masks common cloned DOM controls and token-like text, but it does not inspect, blur, OCR, or redact rendered pixels. Host apps own warnings and exclusion markers for sensitive surfaces.
Capture metadata and fallback:
- On successful automatic capture, create payload metadata includes
target.metadata.screenshot_capture = { status: "attached", scope, image_type }. - On capture/render/upload failure, create payload metadata includes
target.metadata.screenshot_capture = { status: "failed", scope, image_type, failure_reason: "capture_failed" }. - Failure metadata intentionally stores a coarse reason only. Use
onCaptureErrorfor app-local telemetry if you need the raw error. - Capture failures continue issue submission with text/voice metadata and any manual attachments.
- Preserves the SPAPS hosted-asset constraints: private objects, PNG/JPEG/WebP only, 10 MiB per file, and five attachments per report.
- If the blob exceeds
maxBytes, the module compresses automatically (falling back to JPEG at decreasing quality). If compression still exceeds the limit, the error fires throughonCaptureErrorand submission continues without the screenshot.
Operator-gated release:
Publishing this version requires operator approval. Consumers may opt in by passing screenshotCapture={{ enabled: true }} only after the approved package version is published to npm.
Conversation Thread
Support operators can ask a reporter for clarification and post a final resolution message. Those messages flow into an allowlisted, reporter-visible projection that the reporter reads back as a conversation. This package renders that surface when the client implements the two optional message methods:
const client = {
issueReporting: {
...spaps.issueReporting,
listMessages: (issueReportId: string) =>
spaps.issueReporting.listMessages(issueReportId),
submitMessage: (issueReportId, payload) =>
spaps.issueReporting.submitMessage(issueReportId, payload),
},
};When listMessages is present, FloatingIssueReportButton mounts the exported IssueReportMessageThread inside the issue detail modal (the Edit/Reply views). The thread:
- Renders only
active,reporter_visibleprojection rows in chronological order. Retracted and superseded operator messages, non-visible rows, and rawsupport_case_eventspayloads are never displayed. The backend filters this;selectReporterVisibleMessagesre-asserts the same rule in the UI. - Distinguishes operator (
Support) vs reporter (You) authorship, labels each message kind (clarification_request,reporter_response,final_response), and shows a relative timestamp. - Shows a needs-response affordance when the API's
needs_responseflag is set (an open clarification request awaiting an answer), and surfaces a matching dot badge on the floating entrypoint while that thread is open. - Renders a reporter clarification-response composer (shown only when the client implements
submitMessage). It callssubmitMessage(issueReportId, { body, idempotency_key })with a client-generatedidempotency_key. A key reused with a different body returns409 ISSUE_REPORT_MESSAGE_CONFLICT; the composer detects this (isReporterMessageConflict), shows the conflict copy, and rotates the key for retry. - Exposes loading, empty, and error (with retry) states.
The composer does not reopen a closed case; reopen semantics stay with the Reply flow. You can also mount IssueReportMessageThread directly (outside the modal) inside your own issue detail view:
import { IssueReportMessageThread } from "spaps-issue-reporting-react";
<IssueReportMessageThread issueReportId={issue.id} />;All thread strings are overridable through the provider copy prop. The thread copy keys are: threadTitle, threadDescription, threadLoading, threadLoadFailed, threadEmpty, threadNeedsResponseBadge, threadAuthorOperator, threadAuthorReporter, threadKindClarificationRequest, threadKindReporterResponse, threadKindFinalResponse, threadResponseLabel, threadResponsePlaceholder, threadResponseSubmitAction, threadResponseSubmittingAction, threadResponseConflict, and threadResponseFailed.
Exported Surface
| Export | Purpose |
| --- | --- |
| IssueReportingProvider | Owns queries, modal state, copy, scope, and report-mode behavior |
| IssueReportingPageConfig | Per-page override for general_page, surface_required, or surface_preferred create policy |
| FloatingIssueReportButton | Renders the floating entry point, popover, modal, and (when the client supports messages) the conversation thread |
| IssueReportMessageThread | Renders the reporter-visible operator-to-reporter conversation for one issue report, with a clarification-response composer |
| ReportableSection | Marks a region as selectable when report mode is active |
| useIssueReporting | Full provider state, including startNewIssue(), openPageIssueModal(), and enterReportMode() |
| useIssueReportingStatus | Query helper for summary state |
| useIssueReportingHistory | Query helper for filtered history lists |
| useIssueReportingMutations | Mutation helpers for create, update, and reply flows |
| useIssueReportingMessages | Query helper for one issue report's reporter-visible message thread |
| useIssueReportingMessageMutation | Mutation helper for submitting a reporter clarification response |
| selectReporterVisibleMessages | Filters a message list to active, reporter-visible rows in chronological order |
| isReporterMessageConflict | Detects a 409 ISSUE_REPORT_MESSAGE_CONFLICT from the client transport |
| useReportMode | Low-level selection state for custom wrappers |
Styling Notes
- The package ships utility-class based markup for the button, popover, modal, and report-mode highlights.
- If your CSS build only scans local source files, include this package in the scan path or safelist the relevant classes.
FloatingIssueReportButtonacceptsclassNameandpositionClassNamefor placement tweaks.- Provider copy can be overridden with the
copyprop.
Development
cd packages/issue-reporting-react
npm ci
npm run build
npm testTroubleshooting
The button renders but the modal never opens
Make sure FloatingIssueReportButton is rendered inside IssueReportingProvider and that isEligible is true.
I want page-level reporting without wrapping every widget
That is the default. Report This Page opens immediately and captures the current route plus the inventory of visible registered reportable sections on the active page.
Report mode turns on, but sections do not respond
Wrap the target UI in ReportableSection, or call useIssueReporting().selectPanel(...) from a custom control.
This page should force a specific surface selection
Render IssueReportingPageConfig with createMode="surface_required" inside that page's visible provider subtree. Hidden mounted routes, tabs, or panels do not affect the active page's policy.
Tenant scope never appears
The stock SPAPS API supports mine only. Set allowTenantScope={true} only when your client
implements tenant-scoped getStatus and list calls.
Styles look unformatted
Include the package in your Tailwind or CSS-content scan, or override the rendered classes in your host app.
Voice controls do not appear
Pass inputModes={["voice"]} or inputModes={["text", "voice"]} to the provider, and enable voice in the owning SPAPS application's issue-reporting settings.
Voice starts with a token error
Confirm that the SPAPS server has ELEVENLABS_API_KEY set and that the current user is authenticated and eligible for issue reporting.
Screenshot controls do not appear
Expose issueReporting.uploadAttachment on the client passed to IssueReportingProvider. The package intentionally hides the picker when the upload helper is absent.
The conversation thread does not appear
Expose issueReporting.listMessages on the client passed to IssueReportingProvider, then open an existing issue's detail (Edit/Reply). The thread is intentionally hidden when the client cannot list messages. The response composer additionally requires issueReporting.submitMessage; without it the thread renders read-only.
Limitations
- This package assumes React Query is already part of the host app.
- It focuses on the issue-reporting UI flow; queue policy, triage rules, and backend delivery stay outside the package.
- Deep visual changes may require wrapping the exported components rather than using props alone.
FAQ
Do I need the full SPAPS SDK?
No. You only need a client object that implements the issueReporting methods expected by the provider.
Can I launch report mode from my own button?
Yes. Use useIssueReporting().enterReportMode() inside the provider tree.
Can I open a page-level report from my own button?
Yes. Use useIssueReporting().openPageIssueModal() or useIssueReporting().startNewIssue().
Can I report plain strings instead of structured descriptors?
Yes. reportableName accepts either a string or a { componentKey, componentLabel, ... } object.
Can I hide the feature for some accounts?
Yes. Drive that from isEligible.
Does this package manage tenant permissions?
No. It only renders scope choices that your app explicitly allows.
Does this package manage CORS or allowed_origins?
No. This package is UI only. If your browser app calls SPAPS directly with a publishable key, the relevant SPAPS application row must include that browser origin in allowed_origins.
Design system
This package is intentionally theme-neutral and uses accessible Radix behavior
primitives plus a restrained slate palette so it blends into the host app. The
Design System Registry (DSR) adoption decision for this surface is an explicit
deferral because no suitable theme-neutral registry item exists yet. The
repository-level docs/DSR-DECISION.md captures the reasoning, file-level audit,
and revisit trigger.
Metadata
package_name:spaps-issue-reporting-reactlatest_version:0.6.2minimum_runtime:Node.js >=18.0.0api_base_url:https://api.sweetpotato.dev
About Contributions
About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via
ghand independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
License
UNLICENSED
