Files

82 lines
4.3 KiB
Markdown

# QBit QA Capture: design
Provider-pluggable QA bug capture for QQQ applications, Jam (https://jam.dev) first. This QBit
and the QQQ Next UI (`QRun-IO/qqq-frontend-next`) implement one shared contract: the `qa-capture`
UI API extension 1.0.0 (`src/main/resources/openapi/qqq-v1/qa-capture.yaml`) plus the raw routes
below. Owner priority 2026-10-15.
## Goals
- Off by default and zero cost when off: a client that does not see the `qa-capture` extension,
or gets `enabled: false`, makes no further calls and loads nothing from the provider.
- Capture only for chosen users: an administrator audience (users, roles) or self-serve QA mode
for holders of `qa_capture.use_qa_mode`.
- Secrets and secure values never leave the browser: sanitizer rules are data (a standard table),
served to the client, applied before the provider receives anything (fail closed: Jam redacts
when a sanitizer throws).
- Every bug links to server logs: `X-Request-Id` on every API response, kept by the client and
stored on the report.
- All QBit data in standard, visible QQQ tables; no filesystem; DB changes only through the
Liquibase changelog.
## Components
| Component | Class |
|---|---|
| Config and producer | `QaCaptureQBitConfig`, `QaCaptureQBitProducer` |
| Tables, possible values, app, permissions | `metadata.QaCaptureMetaData`, `metadata.QaCapturePermissionChecker` |
| Write validation (rule values, trimming) | `metadata.QaCaptureWriteValidator` |
| Help bundle | `help/qbit-qa-capture.yaml`, `metadata.QaCaptureHelp` |
| Enablement (pure) | `service.QaCaptureEnablement` |
| Capability, reports, webhook context | `service.QaCaptureService` |
| Sanitizer rule set | `service.SanitizerRules`, `web.SanitizerScript` |
| Secured-data rules | `service.SecuredDataRuleSync` |
| QA mode preference | `service.QaModePreference` |
| UI API extension | `api.QaCaptureUiApi`, `api.CapabilitySpec`, `api.ReportCreateSpec`, `api.QaCaptureSchemas` |
| Raw routes | `web.QaCaptureRouteProvider`, `web.RecorderPage`, `web.QaCaptureRequestIds` |
| Dashboard CSP | `QaCaptureCsp` |
| Provider SPI | `spi.QaCaptureProvider`, `spi.JamProvider`, `spi.SvixSignatureVerifier` |
## Enablement
For the configured environment's settings row: `OFF` mode gives reason `OFF`; no row, no team id
or an unknown provider gives `NOT_CONFIGURED`. Otherwise an active administrator audience row
matching the user (audience modes) gives `AUDIENCE`; in self-serve modes a user with
`qa_capture.use_qa_mode` gets `QA_MODE` when their QA mode is on and `QA_MODE_OFF` when it is off;
everyone else gets `NO_ACCESS`.
QA mode is the `qa-capture` user preference section (one boolean), visible only with
`qa_capture.use_qa_mode` (core's section permission). It is stored in core's key-value preference
store when the host defines it, else as a `qa_capture_audience` row with source `QA_MODE`. No
fifth table.
## Request correlation
The route provider adds before and after handlers on `/qqq/*` and `/qa/*`: a valid incoming
`X-Request-Id` (`[A-Za-z0-9._:-]{8,128}`) is reused, else a UUID is generated; the id is set as the
response header and in the log4j ThreadContext (`requestId`) for the request and removed afterwards
(after handlers also run after exceptions). Storing request ids on audits needs core support; see
the QRun-IO/qqq issue linked from the pull request.
## Recorder page and CSP
`GET /qa/recorder` is a registered route, so the Next dashboard's SPA fallback (which only handles
requests no endpoint matched) never serves it, including its 404. Its inline module script is not
user dependent (it embeds the active rules), and its CSP allows it by sha256 hash.
`QaCaptureCsp.customize` adds the provider origins to the dashboard CSP only while capture is
active. The dashboard rebuilds its headers when the QInstance is set (startup and hot swap), so
settings changes reach the dashboard CSP on the next restart or hot swap.
## Webhooks
`POST /qqq/hooks/qa-capture/{provider}` delegates to the provider. Jam: Svix HMAC-SHA256 over
`{svix-id}.{svix-timestamp}.{body}` with the `whsec_` secret, constant-time comparison against
every `v1` signature, 5-minute timestamp tolerance, 1 MB cap; only `jam.created` for the
configured team is processed; idempotent on `jamId` (unique column).
## Placement
The QBit adds one app (`qaCapture`) with its four tables and no processes; `parentAppName` nests it
under the host's Admin app.