feat: qbit-qa-capture 0.1.0 (Jam QA capture, sanitizers, request ids, QA reports) #3

Merged
KofTwentyTwo merged 14 commits from feature/initial-implementation into develop 2026-10-15 17:24:01 +00:00
KofTwentyTwo commented 2026-10-15 17:22:11 +00:00 (Migrated from github.com)

Summary

First implementation of qbit-qa-capture 0.1.0: optional, provider-pluggable QA bug capture (Jam first), built to the shared qa-capture contract v1.0.0 that qqq-frontend-next implements.

  • Tables (qa_capture_settings, qa_capture_audience, qa_capture_sanitizer_rule, qa_capture_report) via db/liquibase/qbit-qa-capture/qa_capture.changelog.sql: 50 built-in sanitizer rules seeded insert-only, one guarded FK (audience to settings, cascade), runnable as an app DB user.
  • Permissions qa_capture.manage, qa_capture.use_qa_mode, qa_capture.view_reports; app qaCapture nested under the host Admin app through parentAppName.
  • Enablement (QaCaptureEnablement, unit tested): AUDIENCE / QA_MODE / OFF / NOT_CONFIGURED / NO_ACCESS / QA_MODE_OFF. USER rows match the user id or email (case-insensitive). ROLE rows match withRoleResolver(...), defaulting to the session permission names. The QA mode preference section is qa-capture.
  • UI API extension qa-capture 1.0.0: qaCaptureCapability and qaCaptureReportCreate. The fragment is committed at src/main/resources/openapi/qqq-v1/qa-capture.yaml, with a contract test.
  • Request ids: X-Request-Id on /qqq/* and /qa/*, plus the log4j ThreadContext key requestId and QaCaptureRequestIds.current().
  • /qa/recorder: Jam connected-domain page with a hash-based CSP and no-store; it answers 404 when capture is off. Tested so that the route wins over the Next dashboard fallback.
  • Jam webhook /qqq/hooks/qa-capture/jam: Svix verification (constant time, 5-minute tolerance, several signatures), 1 MB cap, idempotent on jamId.
  • QaCaptureCsp.customize(headers[, qInstance]): adds the dashboard CSP origins only while capture is active.
  • Secured-data rules: at startup, adds rules for fields marked isSensitive or with a field-level PII/PHI classification. It uses core metadata only and does not depend on qbit-secured-data.
  • Help bundle with a coverage test. Guardrail tests check that every table is used, seeded or explained, and documented.

Additive notes on the contract

  • The qa_capture_audience table has an extra source column (ADMIN or QA_MODE). Without a core key-value preference store, QA mode is stored as a QA_MODE row. These rows never grant audience membership.
  • The nullable providerConfig and sanitizer fields use allOf plus nullable in the fragment, because OpenAPI ignores siblings of $ref.

Tests

./mvnw verify passes: 35 tests, 94% instruction coverage, all classes covered. The suite includes PostgreSQL through Testcontainers (run as a non-superuser) and the JS sanitizers executed in Node.

🤖 Generated with Claude Code

## Summary First implementation of `qbit-qa-capture` 0.1.0: optional, provider-pluggable QA bug capture (Jam first), built to the shared `qa-capture` contract v1.0.0 that `qqq-frontend-next` implements. - **Tables** (`qa_capture_settings`, `qa_capture_audience`, `qa_capture_sanitizer_rule`, `qa_capture_report`) via `db/liquibase/qbit-qa-capture/qa_capture.changelog.sql`: 50 built-in sanitizer rules seeded insert-only, one guarded FK (audience to settings, cascade), runnable as an app DB user. - **Permissions** `qa_capture.manage`, `qa_capture.use_qa_mode`, `qa_capture.view_reports`; app `qaCapture` nested under the host Admin app through `parentAppName`. - **Enablement** (`QaCaptureEnablement`, unit tested): AUDIENCE / QA_MODE / OFF / NOT_CONFIGURED / NO_ACCESS / QA_MODE_OFF. USER rows match the user id or email (case-insensitive). ROLE rows match `withRoleResolver(...)`, defaulting to the session permission names. The QA mode preference section is `qa-capture`. - **UI API extension** `qa-capture` 1.0.0: `qaCaptureCapability` and `qaCaptureReportCreate`. The fragment is committed at `src/main/resources/openapi/qqq-v1/qa-capture.yaml`, with a contract test. - **Request ids:** `X-Request-Id` on `/qqq/*` and `/qa/*`, plus the log4j ThreadContext key `requestId` and `QaCaptureRequestIds.current()`. - **`/qa/recorder`:** Jam connected-domain page with a hash-based CSP and `no-store`; it answers 404 when capture is off. Tested so that the route wins over the Next dashboard fallback. - **Jam webhook** `/qqq/hooks/qa-capture/jam`: Svix verification (constant time, 5-minute tolerance, several signatures), 1 MB cap, idempotent on `jamId`. - **`QaCaptureCsp.customize(headers[, qInstance])`:** adds the dashboard CSP origins only while capture is active. - **Secured-data rules:** at startup, adds rules for fields marked `isSensitive` or with a field-level PII/PHI classification. It uses core metadata only and does not depend on qbit-secured-data. - **Help bundle** with a coverage test. Guardrail tests check that every table is used, seeded or explained, and documented. ## Additive notes on the contract - The `qa_capture_audience` table has an extra `source` column (ADMIN or QA_MODE). Without a core key-value preference store, QA mode is stored as a QA_MODE row. These rows never grant audience membership. - The nullable `providerConfig` and `sanitizer` fields use `allOf` plus `nullable` in the fragment, because OpenAPI ignores siblings of `$ref`. ## Tests `./mvnw verify` passes: 35 tests, 94% instruction coverage, all classes covered. The suite includes PostgreSQL through Testcontainers (run as a non-superuser) and the JS sanitizers executed in Node. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.