4.3 KiB
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-captureextension, or getsenabled: 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-Idon 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.