82 lines
4.3 KiB
Markdown
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.
|