Files

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-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.