Skip to content

Commit 4933e51

Browse files
authored
enhancement(audit): comprehensive audit logging with inline activity views (#466)
* enhancement(audit-logs): add per-project audit log view - New project-scoped Audit Logs page (project menu item) for system admins and for Project Administrators on their assigned projects, backed by a PROJECTADMIN read policy on AuditLog - Add Project filter and Project-column sorting to the admin and profile audit log views - Default both the admin and project views to no date window; the infinite scroll + virtualized table handle the full range - Add searchProjectAuditLogUsers server action (raw SQL, restricted to ADMIN or an assigned PROJECTADMIN) for the project user filter, with unit tests covering the authorization boundary - Document the project audit log under Projects and link it from the permissions guide * enhancement(audit): per-case activity log with content-change capture Add a per-case Activity sheet to the test-case detail page plus the audit plumbing behind it: - RepositoryCaseAuditLogSheet: lazy right-side sheet showing the case's audit trail, gated by a new AuditLog read policy so anyone who can view the case can read its audit entries (mirrors the RepositoryCases read rules, scoped to entityType='RepositoryCases'). - Audit CaseFieldValues mutations per-row via ENTITY_AUDIT_MODELS, with project scope backfilled through the parent test case. - Consolidated 'case content changed' entry: after a save, /api/audit/case-version diffs consecutive version snapshots (custom fields, steps, tags, issues, parameters) into one readable AuditLog row. - Restrict audit metadata (IP / user agent) to admins in the detail modal. - Version selector shows a compact 'v{n}' badge. i18n: repository.auditLog (en-US). * enhancement(audit): database-level change-data-capture substrate Record every mutation to audited tables (cases, runs, sessions, their child/value tables, and the tag/issue link tables) into an immutable, append-only DataChangeLog via Postgres triggers — capturing writes from every path (hooked client, raw SQL, workers, bulk operations) that the app-layer audit hooks structurally miss. - DataChangeLog model + a generic audit_row_change() trigger: changed-column {old,new} diffs, a per-table column denylist + 32KB size guard, a recursion guard, and a skip_audit hatch for high-volume imports - Table registry + an idempotent apply-triggers script (via DIRECT_DATABASE_URL), chained into pnpm generate and the deploy entrypoints, plus a drift test - Append-only enforcement at the database level (UPDATE/DELETE raise a privilege error) alongside an INSERT-only grant - Actor / operation / tenant attribution via SET LOCAL app.audit_context in the hooked-client transactions and worker entry points Runs alongside the existing app-layer audit; nothing consumes DataChangeLog yet. * enhancement(audit): correlate change-data-capture into readable audit entries Materialize the immutable DataChangeLog into the existing AuditLog so the trigger-captured changes become a human-readable account of who changed what. - A restart-safe worker loop polls DataChangeLog (FOR UPDATE SKIP LOCKED), rolls child-row changes up to their owning case/run/session, humanizes FK values (field and state names), and writes AuditLog rows — the insert and the cursor advance happen in one transaction so a crash never duplicates or drops a row - Per-operation grouping: a client operationId (X-Operation-Id header via a provider-level fetcher) threads through the audit context onto every change row, so a multi-request save collapses into a single entry; the audit views group by it - AuditLog gains operationId and sourceTable; an idempotency index guards re-materialization - Soft-deletes (isDeleted flips) surface as deletions in the trail Still runs alongside the existing app-layer audit (intentional double-capture until reconciliation). * enhancement(audit): extend CDC triggers to all audited entities + add hook/trigger reconciliation - Extend the audit trigger registry from 23 to 68 tables, covering the remaining currently-audited data entities (users, projects, config catalogs, issues, milestones, comments, attachments, permissions and their child/value tables). Credential columns (Integration/LlmIntegration/CodeRepository), User.lastActiveAt, and rich-text columns are denylisted; credential/token tables stay excluded from triggers entirely. - Add scripts/reconcile-audit-coverage.ts (pnpm reconcile:coverage): a runtime parity report that partitions AuditLog rows by sourceTable (app-hook-sourced vs trigger-sourced) and proves trigger capture is a superset of the hook capture, exiting non-zero on any gap. - Add a credential-denylist integration test proving a write to a credential-bearing table never records the credentials column into the change log. - Tag the existing raw-SQL and bulk-createMany capture assertions for the client/method-agnostic coverage requirement. * enhancement(audit): retire the legacy app-layer data audit in favor of database triggers The Postgres trigger substrate now captures every data-change audit event, so the parallel app-layer hooks are removed: - Remove the per-entity Prisma $extends data-audit hooks, the ENTITY_AUDIT_MODELS entries, and the DATA-entity RPC audit shim. Actor attribution (the SET LOCAL GUC), Elasticsearch sync, outbound webhooks, and all semantic/security events (login, export, role/permission changes, SSO, SCIM, API-key lifecycle) are preserved. - Delete the interim per-case audit pieces (the CaseFieldValues hooks and the /api/audit/case-version endpoint); the per-case Activity view reads the materialized audit log directly. - Apply the audit triggers in the E2E harness setup so the audit specs run against the trigger path. - Add a 30-day retention worker for the change-log table, with a carve-out in the append-only enforcement that permits pruning processed rows while still forbidding deletion of unprocessed ones. * fix(audit): record the acting user on the trigger-audit RPC path + fix the prod deploy crash - Move `pg` from devDependencies to dependencies. The production container's entrypoint runs the trigger-apply script, which requires `pg`; with it pruned from the production image the container crash-looped on every deploy. - Attribute trigger-captured changes to the acting user on the model API route. ZenStack's enhance() client (and the fast-path create) bypass the Prisma extension that set the audit-context GUC, so the captured actor was empty. enhanceWithAudit runs each write inside a transaction that sets the GUC first and re-issues the write on the transaction client, so the policy check and the write share it and the trigger records who made the change. Reads pass through; nested writes don't re-wrap (preserving caller transaction atomicity). - Record changes that have no originating user (background jobs, seeds, migrations) under the existing `__system__` actor sentinel instead of an empty actor, with a regression test. * fix(audit): bind hooked-client writes to the audit transaction The per-entity Prisma $extends hooks opened a transaction, set the app.audit_context GUC on it, then ran the write via query(args) — which executed OUTSIDE that transaction (autocommit). So the write and its audit / Elasticsearch-sync / webhook side-effects were not atomic, and the trigger-captured DataChangeLog row recorded no actor (the GUC was read from a transaction the write never ran in). Re-issue each hooked write on the transaction client (tx[model][op](args)) so the write, the GUC, and the side-effects share one transaction: the write is now atomic with its side-effects, and trigger CDC records the acting user whenever the request established an audit-context frame. Reads are unaffected; tx is the un-extended base client, so this does not recurse. * fix(audit): attribute the actor at the transaction boundary, not per write hook The per-hook GUC wrapper wrapped each hooked write in its own baseClient.$transaction. Inside a route's own prisma.$transaction this split the hooked parent write into a separate transaction: sibling child/value-table writes (CaseFieldValues, Steps, iterations, ...) stayed in the route's un-GUC'd outer tx and recorded a null actor (-> __system__), and the parent committed independently of the route's transaction (atomicity loss + deadlock risk under a small/pgbouncer pool). Set app.audit_context once at the transaction boundary instead, and have the hooks reuse that transaction: - auditedTransaction(fn) and auditedEnhancedTransaction(session, fn) open the transaction, SET LOCAL app.audit_context as the first statement, and publish the tx on a shared AsyncLocalStorage (auditTxStore). The enhanced variant keeps ZenStack policy enforcement. Raw prismaBase paths (merge, step-sequence conversion) use withAuditGuc(prismaBase, buildGucPayload(userId)) to keep their alias/policy exemption. - The 15 GUC-wrapped $extends hooks (withHookTx) now run on the ambient audited transaction when one exists -- single tx, full parent+child attribution, atomicity preserved -- and only open their own when the write is standalone. - Convert the request-scoped transactional mutation paths that write audited tables to these helpers (bulk-edit, submit-result, edit-result, iterations, parameters, dataset import, merge, step-sequence conversion, milestones, reviews, project-integration, prompt-config, auto-tag, admin user create, generate-iterations, Jira import, ...), adding an audit frame where the handler lacked one. SCIM provisioning and anonymous signup remain unattributed (recorded as __system__): IdP/automation, not a user session. Validated end-to-end on the prod stack: a bulk edit of a case's Priority now writes the RepositoryCases and CaseFieldValues rows under one transaction id with the editing user as the actor (previously the child row carried an empty actor in a separate transaction). * fix(audit): snapshot actor/entity/project names at write time so the audit log is immutable The trigger-based CDC migration recorded only ids — actor userId, table + pk — and left the readable AuditLog's userName/userEmail/entityName/projectId empty, expecting them to be resolved later. That broke the per-project audit view (it filters on projectId, which was always null) and showed no entity/user names; worse, resolving them at read time would show each value as it is NOW, not as it was when the change happened, defeating the point of an immutable audit log. Capture the human context AT WRITE TIME and copy it through verbatim — no lookup at display or in the correlation worker: - DataChangeLog gains actor_name, actor_email, entity_name, project_id. The audit_row_change() trigger snapshots actor name/email from the app.audit_context GUC and reads entity_name/project_id straight off the changed row's configured name/project columns (per-table nameCol/projectCol in scripts/trigger-registry.ts, passed as trigger args). Reading off the row is immutable (the value as it was at that instant) and handles bulk edits row-by-row with no per-route fan-out. A child/value/join table whose own row carries neither falls back to the GUC subject set by the few child-only operations (recording a result). - buildGucPayload carries userName/userEmail (and an optional subject) into the GUC; the session callback already enriches the frame, enhanceWithAudit and auditedEnhancedTransaction pass the explicit user, and the model route stamps name/email on its frame before tryFastPathCreate runs (which otherwise saw none). - The correlation worker copies the four fields straight into AuditLog; child rows inherit their owning entity's snapshot from the same operationId group. - submit-result / edit-result set the run as the audit subject, and the result modals mint one operationId per "record result" action so the result and its step-result writes group together — the step rows then inherit the run's name and the whole action reads as one grouped audit entry. Validated end-to-end on the prod stack: case create/edit/delete, sessions, and result submissions all record the acting user's name, the entity's name as it was then, and the project — every AuditLog row fully attributed, nothing looked up. * enhancement(audit): audit webhook config + IntegrationProject changes with the acting admin Webhook configuration changes (create/delete/rotate/activate) produced no audit record at all: WebhookConfig was absent from the trigger registry and the webhook-config server actions audit only one of their ~17 functions. The IntegrationProject external-project mapping was likewise unaudited. - Add WebhookConfig and IntegrationProject to scripts/trigger-registry.ts. The webhook token + secret columns are denylisted so credential material never lands in the append-only DataChangeLog (SAF-02/04); the dedicated WebhookConfigSecret table stays excluded. WebhookConfig carries name + projectId; IntegrationProject self-attributes by its externalProjectName (projectId is two hops away via projectIntegration). - WebhookConfig is written through the hooked client but had no hook, so its writes never set the app.audit_context GUC and would record __system__. Add a minimal webhookConfig GUC hook (via the shared withHookTx) that only sets the context, and have the 11 webhook-config runWithAuditContext frames carry the acting user's name + email, so these changes attribute to the admin. Validated: creating an outbound webhook now records CREATE WebhookConfig "<name>" by the acting admin in the right project, with the token/secret absent from the captured diff. * enhancement(audit): make the correlated log read as one action per save UAT review surfaced several readability problems in the materialized AuditLog. This addresses them in the correlation worker and the table UI: - Group rows with no browser operationId by their transaction id (a parent and the children written in the same transaction are one atomic save), so the children inherit the parent's name/project and the UI collapses them into one entry. The synthetic `tx:<txid>` operationId is stamped on those rows. - Cancel no-op association churn: when a save re-applies an UNCHANGED m2m link it writes a join-table DELETE + CREATE of the same link in one operation; drop the matched pairs so a rename no longer reads as "removed tag / added tag". One-sided adds/removes are preserved. Cancelled rows are still marked processed. - Comment attribution: a comment carries its creatorId (the actor) and exactly one parent FK (the case/run/session/milestone it is on) but no names and no GUC actor — so roll it up to that parent and resolve the creator + parent names (the one deliberate, comment-only lookup; createPrismaLookup gains those tables). - Drop SessionVersions from the trigger registry — a version snapshot the app writes in its own transaction on every session save, producing a redundant no-name audit row (mirrors RepositoryCaseVersions, already excluded). apply- triggers now also drops orphaned tpl_audit_* triggers so a registry removal is clean and the drift self-check stays in lockstep. - VirtualizedDataTable: nested sub-rows get a shaded background and a wide colored bar on the right edge of the indent column, so a run of detail rows reads as one group and the next parent row is clearly the boundary. * enhancement(audit): capture and label custom field-value changes in case logs Value-only updates to a value table (CaseFieldValues / ResultFieldValues / SessionFieldValues) only write `value`, so the captured diff lacked the field identity and the rollup FK, and rolled up to the row's own pk instead of its owning case/run/session. - Trigger carries a per-table capture list (rollup FK + fieldId) on every UPDATE so value-only edits still attribute to their owner and name the field. - Correlation re-keys value-table diffs under the field's display name with the resolved label ("Priority: Medium -> High") instead of an opaque "value: 3 -> 2". - Cross-batch owner back-fill rolls name-carrying rows up to their owning entity so children written in a later poll batch (e.g. step results) still inherit the owner name rather than showing blank. * enhancement(audit): make CDC the sole source for catalog/config audit and resolve access-control ids to names The catalog/config/access-control models were audited by BOTH the app-layer config hooks (AUDITED_CONFIG_MODELS) and the database CDC triggers, so every such change produced a duplicate AuditLog row. The CDC trigger already captures the same create/update/delete under the acting admin (the request GUC is set by the route, not these hooks), so the app-layer config audit is retired here — the app layer now records only semantic / security events. Assignment/join tables (GroupAssignment, RolePermission, Project*Assignment, ...) have no name column, so their CDC rows showed raw FK ids. The humanizer now resolves userId / groupId / roleId / projectId / configurationId / milestoneTypeId to names, so an access-control change reads "user: Administrator Account, group: QA" instead of opaque ids. * enhancement(audit): show resolved names (not raw ids) in the audit log detail modal The correlation worker resolves FK ids to display names alongside the raw value (oldName/newName for statusId, fieldId, and the access-control assignment FKs), but the shared audit detail modal rendered only the raw old/new — so a group membership change showed a user cuid instead of "Administrator Account", and a status change showed an id instead of its name. Prefer the resolved name when present, falling back to the raw value on a humanization miss. * fix(audit): attribute session results to their session, not a blank entity or TestRuns ResultFieldValues is a shared value table — a row belongs to a test-run result, a session result, OR a case (testRunResultsId / sessionResultsId / testCaseId). The rollup only knew the test-run FK, so a session result's field values fell back to TestRuns. Correlation now branches by whichever owner FK is set, and the trigger captures all three so a value-only update still attributes correctly. SessionResults also materialized with a blank name: session results go through the generic model route (not a bespoke endpoint), which set no audit subject. The route now reads the session's name + projectId for SessionResults.create and sets them as the GUC subject, so the result and its nested result-field values record the session at write time — matching how submit-result names test-run results. * chore(format): prettier-format CDC-milestone files The CDC audit milestone landed without passing the repo-wide format:check; this runs prettier on the affected files. No behavior change. * chore(lint): remove unused prisma/db/enhance imports Full-repo eslint flagged these imports as unused (the apparent uses were comments or a shadowing local parameter). * test(audit): repair CDC-milestone test mocks and expectations The CDC milestone added app.audit_context GUC injection inside DB transactions and decommissioned the app-layer config audit, but the test mocks/expectations were not updated (250 failures). Adds $executeRaw/$queryRaw to transaction mocks, $extends to enhanced-client mocks, missing mock exports, and updates the config-audit tests to the decommissioned (empty AUDITED_CONFIG_MODELS) design. Test-only changes; no source modified. * fix(audit): record access changes once via CDC, not double-audited Decommission the app-layer ROLE_CHANGED / PERMISSION_GRANT / PERMISSION_REVOKE events (gated by an empty SEMANTIC_ACCESS_AUDIT_MODELS set, mirroring AUDITED_CONFIG_MODELS) so an access change is recorded once by the CDC substrate instead of twice. The correlation layer derives a readable "User -> Project" label + projectId for the nameless permission/membership rows from the FK display names it already resolves. * fix(audit): group aggregate semantic events with their CDC detail rows Stamp operationId from the request audit context onto semantic-event rows so BULK_*/DUPLICATED/ITERATION/copy-move summaries group as the header of their CDC per-row detail rows instead of floating with a null operationId. * fix(audit): carry the full actor context on copy/move CDC writes copyMoveWorker set a partial audit GUC (userId only), leaving copied rows with a blank actor name and an ungrouped operationId. Build the payload with buildGucPayload() (the processor runs inside runWithAuditContext) so the copied case + children carry the actor name + operationId and group with the semantic CREATE/DUPLICATED summary. * fix(audit): roll up project-config assignments to the project, preserving every setting The Project*Assignment join tables (workflow/status/milestone-type/user) flooded the audit log with thousands of blank-named self-attributing rows on every project setup. They now roll up to their owning Projects row (named, grouped), with the specific setting kept in the humanized diff. A same-owner merge step combines child rows sharing an audit identity (operationId+sourceTable+entityId+action) so EVERY setting is preserved (comma-listed) instead of being dropped by the ON CONFLICT DO NOTHING idempotency index — this also fixes multi-field-value changes losing all but one field. * fix(audit): humanize implicit m2m tag/issue link diffs The 9 implicit Prisma m2m join tables (_*To* for tags and issues) rendered opaque generic A/B columns in the diff ("{A:4,B:13}"). humanize() now collapses each to just the linked entity, named — { Tags: "regression" } / { Issues: "PROJ-123" } — dropping the redundant owner column (the row already attributes to its case/run/session). * fix(audit): roll up project-configuration assignments to the project ProjectConfigurationAssignment is an audited project-setup join table of the same nameless {configurationId, projectId} shape as the workflow/status/ milestone-type/user assignment tables, but was missing from the rollup map — so it flooded the audit log with self-attributing blank-named rows on project setup instead of grouping under the owning Projects row. Add it to ROLLUP_MAP and the access-label project-name fallback alongside the other four. * fix(audit): denylist User credential columns from CDC capture The User audit trigger captured raw column values, so password (bcrypt hash), twoFactorSecret, and twoFactorBackupCodes — all @omit in schema — landed in the append-only DataChangeLog and surfaced in AuditLog. Add them to the User trigger denylist (SAF-02/04), mirroring the semantic audit's SENSITIVE_FIELDS. The change EVENTS remain audited semantically (PASSWORD_CHANGED, TWO_FACTOR_ENABLED, …) with the value redacted, so no audit coverage is lost. * fix(audit): recursively redact secrets nested inside JSON columns maskSensitiveValue only masked top-level columns whose name was sensitive, so a secret nested in a Json column passed through untouched — SsoProvider.config logged clientSecret in plaintext. Walk plain JSON objects/arrays and mask any nested key in SENSITIVE_FIELDS (Dates/Decimals left intact); add clientSecret to the set. Change detection still compares raw values, so diffs are unaffected. * test(audit): update audit-log page E2E to VirtualizedDataTable selectors The audit-log-management E2E specs were stale since the page migrated from a semantic-table DataTable to VirtualizedDataTable (PR #441): they waited on getByRole('table')/thead/tbody, which the virtualized ARIA-rolled div structure never renders, so all 7 timed out. Target the real testids/roles instead (audit-logs-table, -scroll, audit-log-row-<id>, [role=columnheader]) and add a data-testid to the row view-details button. All 8 tests pass against a fresh build with audit data present (real detail-modal + export paths exercised). * fix(audit): make the CDC pipeline multi-tenant aware DataChangeLog lives in every tenant database (the capture triggers are applied per-DB), but both the CDC consumer and its retention worker ran only against the primary database — so in multi-tenant deployments every tenant's audit changes were captured yet never correlated into their AuditLog and never purged, growing the log unbounded. - Loop B (correlation consumer): replace the single-client poll loop with pollDataChangeLogsAcrossTenants, a supervisor that re-resolves the live tenant set each cycle (runtime additions picked up without a restart, mirroring the webhook outbox poller) and runs one poll pass per tenant with per-tenant error isolation. Single-tenant mode is a one-element client list. - DataChangeLog retention worker: add purgeAllTenantsOnce — iterate every tenant DB via getTenantPrismaClient with a per-tenant time budget, one DCL_RETENTION_ PURGED audit row per (tenant, run), and disconnect tenant clients after each daily pass. Mirrors webhookRetentionWorker. - Bump audit-log-worker to the 3G tier: Loop B now caches one raw Prisma client per tenant (one Rust query engine each), the same footprint as the webhook outbox worker; harmless headroom in single-tenant mode. - Wire dcl-retention into start:workers:prod for parity with ecosystem.config.js, and document both workers' multi-tenant behavior. Removes the now-superseded single-client pollDataChangeLogs. * test(audit): E2E coverage for the project audit log and case history surfaces Neither new audit surface had E2E coverage (repository-history.spec.ts tests browser navigation, not the audit sheet). Add Playwright specs that exercise both against a production build: - Project > Audit Log: the ADMIN-gated page renders the project-scoped trail, the virtualized table + headers, the project's own audit rows, the detail modal, and action filtering. - Repository Case > Activity: the history sheet opens, renders the case-scoped trail, surfaces the case's audit rows, and opens the detail modal (scoped by title since the sheet is itself a role=dialog). Add a data-testid to the case history sheet trigger for a stable selector. Both verified green with real CDC-materialized audit data. * fix(audit): audit UserIntegrationAuth (OAuth tokens) connect/refresh/revoke UserIntegrationAuth is a credential table excluded from CDC triggers (SAF-04), and its real mutations run through the raw prismaBase client in AuthenticationService — so they bypassed the $extends entity-audit hooks entirely (on main and this branch alike), leaving integration-credential changes unaudited. Emit an explicit semantic audit at the mutation sites, mirroring the apiToken precedent: storeUserAuth → CREATE (first auth) / UPDATE (re-auth or token refresh), revokeUserAuth → DELETE. The encrypted tokens are never included (entityType + integration name + integrationId only). lastUsedAt bumps are intentionally not audited (housekeeping noise, like lastActiveAt). * fix(audit): attribute CDC actor for RPC-route updates of hooked entities Updates/deletes of the primary entities (Cases, Runs, Sessions, Issues, ...) made through the ZenStack RPC route were recording in the CDC audit log with an empty actor and no operationId (rendered as "System"), while their child/value-table writes in the same save attributed correctly. Two causes: - enhanceWithAudit enhanced the hooked `lib/prisma` client. When ZenStack's enhance() wraps a base client that carries its own $extends query hooks (repositoryCases/testRuns/sessions/issue/...), it routes those models' writes through a path that skips the post-enhance audit-guc $extends, so app.audit_context is never set on the write's transaction. Enhance the raw prismaBase instead so $allOperations fires for every model uniformly; the hooked client's ES-sync/webhook hooks were already bypassed by enhance() (the RPC route carries manual shims), so nothing is lost. - The case editor saved the case via `mutate` (fire-and-forget), racing the begin/endOperation window so the write missed X-Operation-Id grouping; switched to `mutateAsync` so it completes in-window. Adds enhanceWithAudit.test.ts pinning the raw-base-client invariant. * fix(audit): show Issue title instead of reference key in the audit log The Issue CDC trigger captured the `name` column (the reference key, e.g. "#213") as the audit entity name, while every other entity shows its human-readable name/title. Switch the Issue trigger's nameCol to `title` ("[FEATURE] Webhook System") so the audit log reads consistently. Existing rows are immutable and keep their captured value; new captures use the title. * enhancement(audit): wrap long values in the audit log detail modal The old/new change values and the metadata JSON rendered in <pre> blocks that scrolled horizontally on a single line, hiding long values. Wrap them (whitespace-pre-wrap + break-words) so the full value is visible. Also color the case Activity history icon with the foreground token. * enhancement(audit): per-run and per-session activity history sheets Surface the CDC audit trail scoped to a single test run or session, mirroring the existing per-case Activity sheet. - Extract a generic ScopedAuditLogSheet (entityType + entityId + labels + testids) and refactor RepositoryCaseAuditLogSheet onto it. - Add RunAuditLogSheet + SessionAuditLogSheet wrappers; wire the Activity trigger into the manual run header, the JUnit run header (JunitTableSection), and the session header. - Broaden the AuditLog read policy so anyone who can read a run/session can read its audit trail, using the same project-membership predicate as RepositoryCases. - Add E2E specs for the run and session history surfaces. * enhancement(audit): summarize bulk run-case adds as "N test cases added" Adding cases to a run wrote one merged TestRunCases CREATE audit row whose columns were comma-joined raw ids (repositoryCaseId: "100383, 100080, …") buried under iteration counters — unreadable. - Resolve repositoryCaseId to the case name in the humanizer (reusing the existing RepositoryCases lookup delegate). - Add a summarizeBulkCaseAdds correlation pass that rewrites the merged row into one line: "N test cases added: <case names>". The count comes from the numeric pk list, robust against case names containing commas. Non-create / non-case rows pass through untouched. - Unit test covering bulk collapse, singular, raw-id fallback, and pass-throughs. * enhancement(audit): record run-case removals as "N test cases removed: <names>" Removing test cases from a run is a soft-delete (UPDATE isDeleted), which previously captured only the isDeleted flip — the run history couldn't say which cases left. Mirror the add side so a removal reads "N test cases removed: <case names>", attributed to the user who did it. - The TestRunCases trigger now captures repositoryCaseId, so a soft-delete still records which case left the run. - The correlation worker no longer drops that captured id as unchanged noise on a run-case delete (it stays dropped on any other update). - summarizeBulkCaseChanges (was summarizeBulkCaseAdds) now collapses a bulk CREATE or DELETE into one readable line carrying the comma-listed names. * docs(audit): document Activity views and profile audit log, add v0.40.6 upgrade notification Document the per-item Activity Log (the Activity button on test cases, runs, and sessions) and the profile Audit Log section, and announce both in the in-app upgrade notification for v0.40.6. * chore: build docs as part of precommit Append `pnpm build:docs` (Docusaurus build, which fails on broken links) to the precommit chain so documentation changes are validated alongside the app checks.
1 parent f75ca27 commit 4933e51

182 files changed

Lines changed: 28198 additions & 14111 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/docs/background-processes.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ The application uses the following background processes:
2424
12. **Webhook Dispatch Worker** - Delivers outbound webhooks to subscribed external endpoints
2525
13. **Webhook Outbox Worker** - Polls the outbox table and fans events out to webhook configs
2626
14. **Webhook Retention Worker** - Daily purge of webhook delivery / dedup / outbox rows older than 30 days
27-
15. **Scheduler** - Sets up recurring jobs (cron jobs)
27+
15. **DataChangeLog Retention Worker** - Daily purge of processed audit change-capture rows older than 30 days
28+
16. **Scheduler** - Sets up recurring jobs (cron jobs)
2829

2930
## Workers
3031

@@ -91,6 +92,7 @@ The application uses the following background processes:
9192
- Persists audit log entries for user and system actions
9293
- High throughput, independent operations
9394
- Default concurrency: 10 (lightweight, independent writes)
95+
- Also runs the CDC correlation loop (Loop B): drains each database's `DataChangeLog` into the readable `AuditLog`; in multi-tenant mode it polls every configured tenant per cycle, re-reading the tenant list so runtime additions are picked up
9496
- Location: `workers/auditLogWorker.ts`
9597

9698
### Budget Alert Worker
@@ -137,6 +139,15 @@ The application uses the following background processes:
137139
- Batched `LIMIT 1000` deletes to avoid lock contention
138140
- Location: `workers/webhookRetentionWorker.ts`
139141

142+
### DataChangeLog Retention Worker
143+
144+
- Wakes once per day and batch-deletes processed `DataChangeLog` rows (the audit change-capture substrate) older than 30 days
145+
- Never deletes unprocessed rows — the audit log worker must correlate them into `AuditLog` first; the append-only `datachangelog_append_only` trigger enforces this at the database level too
146+
- Batched `LIMIT 1000` deletes to avoid lock contention with the capture path; emits one `DCL_RETENTION_PURGED` audit event per run
147+
- Multi-tenant aware: runs the purge against every configured tenant database per cycle and emits one audit row per (tenant, run)
148+
- Standalone daily loop (no BullMQ queue) — self-schedules internally rather than via the scheduler
149+
- Location: `workers/dataChangeLogRetentionWorker.ts`
150+
140151
### Scheduler
141152

142153
- Sets up recurring jobs using cron patterns
@@ -232,9 +243,11 @@ Most workers run with a 512 MB `max_memory_restart` ceiling and a 384 MB old-spa
232243
| Sync Worker | 1G | 768M | Loads integration adapters + Elasticsearch sync extensions |
233244
| Forecast Worker | 2G | 1536M | Recomputes run/case forecasts over large historical result sets; the default 512 MB tier was OOM-killed under production data volumes |
234245
| SCIM Access Recompute Worker | 2G | 1536M | Loads the full ZenStack runtime to recompute `User.access` tiers from IdP group mappings; boots to ~1.4 GB RSS at idle, so the default 512 MB tier triggered a tight PM2 SIGINT/restart loop rather than a real OOM |
246+
| Audit Log Worker | 3G | 2304M | The CDC correlation loop (Loop B) caches one raw Prisma client per tenant in multi-tenant mode — one Rust query engine per tenant, the same per-tenant footprint as the webhook outbox worker. Harmless headroom in single-tenant mode (a single client). |
235247
| Webhook Dispatch Worker | 3G | 2304M | Loads ZenStack runtime + ES sync services + audit log service; carries full `test_run.completed` payloads under concurrency=5; observed steady-state RSS in multi-tenant clusters sits near 1.9 GB |
236248
| Webhook Outbox Worker | 3G | 2304M | Same heavy dependency tree as dispatch; caches one Prisma client per tenant in multi-tenant mode |
237249
| Webhook Retention Worker | 3G | 2304M | Iterates every tenant's database per pass; headroom protects against batched-delete loops on tenants with large retention backlogs. Tenant Prisma clients are disconnected after each pass to release Rust query engine buffers, so steady-state should drop substantially after the first few passes — these ceilings can be lowered once production telemetry confirms it. |
250+
| DataChangeLog Retention Worker | 3G | 2304M | Loads the audit log service plus the raw Prisma base client and runs a batched-delete loop over the high-volume change-capture table; headroom mirrors the webhook retention worker so a large purge backlog (e.g. after a heavy import) does not trip the ceiling mid-pass. |
238251

239252
## Persistence Across Reboots
240253

@@ -298,7 +311,7 @@ You can monitor worker health and performance using:
298311

299312
### Memory issues
300313

301-
- Most workers run with a 512 MB ceiling; the sync worker runs with a 1 GB ceiling; the forecast and SCIM access recompute workers run with a 2 GB ceiling; the three webhook workers (dispatch, outbox, retention) run with a 3 GB ceiling. See the **Worker memory tiers** table above for the rationale on each elevated tier.
314+
- Most workers run with a 512 MB ceiling; the sync worker runs with a 1 GB ceiling; the forecast and SCIM access recompute workers run with a 2 GB ceiling; the audit log worker, the three webhook workers (dispatch, outbox, retention), and the DataChangeLog retention worker run with a 3 GB ceiling. See the **Worker memory tiers** table above for the rationale on each elevated tier.
302315
- If a worker is being killed and restarted by PM2 in a tight loop (visible as repeated `restart` events in `pm2 status`), raise `max_memory_restart` and `--max-old-space-size` in `ecosystem.config.js` for that worker before assuming there is a real leak.
303316
- Monitor with `pm2 monit`
304317

docs/docs/features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ TestPlanIt is a comprehensive test management platform designed to help teams pl
189189
- **Password enforcement** - Force password changes (individual or bulk) and revoke passwords
190190
- **Password strength indicator** - Real-time zxcvbn-powered feedback on signup and password change forms
191191
- **Sign-in enforcement** - [Force SSO](./user-guide/security-settings.md#sign-in-enforcement) (disable email/password sign-in), require 2FA for password logins, or require 2FA for all sign-ins including SSO. All three toggles live alongside the password policy on the Security admin page
192-
- **Audit logs** - Track all changes for compliance and security review; covers admin configuration mutations, system-actor attribution, SCIM mutations (filterable by `source = scim` with the originating token id), and a tamper-evident archive surface for long-term retention
192+
- **Audit logs** - Track all changes for compliance and security review; covers admin configuration mutations, system-actor attribution, SCIM mutations (filterable by `source = scim` with the originating token id), and a tamper-evident archive surface for long-term retention. Surfaced system-wide for admins, [per project](./user-guide/audit-logs.md#viewing-a-projects-activity), [per user](./user-guide/audit-logs.md#viewing-your-own-activity) from any profile, and [per item](./user-guide/audit-logs.md#viewing-an-items-activity) via the Activity view on test cases, runs, and sessions
193193
- **Two-factor authentication** - Add an extra layer of security for user accounts
194194
- **Data encryption** - Secure data at rest and in transit
195195
- **API tokens** - [Personal and service-account API tokens](./api-tokens.md) with scoped access and revocation

docs/docs/multi-tenant-workers.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,14 @@ All workers support multi-tenant mode:
164164
| Sync Worker | Yes | Syncs issues to correct tenant database |
165165
| Elasticsearch Reindex Worker | Yes | Indexes to tenant-specific ES index |
166166
| Auto Tag Worker | Yes | Runs AI tagging against correct tenant database |
167-
| Audit Log Worker | Yes | Persists audit entries to correct tenant database |
167+
| Audit Log Worker | Yes | Persists semantic audit entries to the correct tenant database (per-job `tenantId`); its CDC correlation loop (Loop B) drains every configured tenant's `DataChangeLog` into that tenant's `AuditLog` each cycle, re-reading the tenant list so runtime additions are picked up |
168168
| Budget Alert Worker | Yes | Checks budgets per tenant database |
169169
| Repo Cache Worker | Yes | Refreshes tenant-scoped Valkey caches |
170170
| Testmo Import Worker | Yes | Memory-intensive; consider per-tenant deployment for frequent imports |
171171
| Webhook Dispatch Worker | Yes | Routes per-job via `tenantId` stamped on each dispatch job by the outbox poller |
172172
| Webhook Outbox Worker | Yes | Polls every configured tenant per cycle, claims per-tenant batches via `FOR UPDATE SKIP LOCKED`, stamps `tenantId` on each enqueued dispatch job |
173173
| Webhook Retention Worker | Yes | Runs the daily 30-day purge against every configured tenant database, emits one audit row per tenant per run |
174+
| DataChangeLog Retention Worker | Yes | Runs the daily 30-day purge of the audit change-capture log against every configured tenant database, emits one audit row per tenant per run |
174175

175176
### Testmo Import Worker Note
176177

docs/docs/user-guide/audit-logs.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,22 @@ The system-wide viewer is available only to users with administrative privileges
2929

3030
### Viewing your own activity
3131

32-
You don't need administrator access to review your own audit history. Open your profile (**user menu → View Profile**) and expand the **Audit Log** section. This view is automatically scoped to your own actions, so the user filter is omitted; the search box and the action, entity-type, and date-range filters work just like the system-wide viewer.
32+
You don't need administrator access to review your own audit history. Open your profile (**user menu → View Profile**) and expand the **Audit Log** section. This view is automatically scoped to your own actions, so the user filter is omitted; the search box and the action, entity-type, project, and date-range filters work just like the system-wide viewer.
3333

3434
Administrators can review any user's activity the same way by opening that user's profile.
3535

36+
### Viewing a project's activity
37+
38+
Project administrators don't need system-wide access to audit a single project. Open the project and choose **Audit Logs** from the project menu to see the audit trail scoped to that project.
39+
40+
This view is available to system administrators and to Project Administrators assigned to the project. It works like the system-wide viewer — the search box and the action, entity-type, user, and date-range filters all behave the same — but every entry belongs to the current project, so the Project column and Project filter are omitted. The menu entry appears for any Project Administrator, but it shows entries only for the projects they are assigned to.
41+
42+
### Viewing an item's activity
43+
44+
You can review the change history of an individual test case, test run, or session without leaving its page. Open the item and click the **Activity** button in the header to open its **Activity Log** — a slide-out panel scoped to that one item.
45+
46+
This view is available to anyone who can open the item — no administrator access required. It lists every recorded change to the item and its contents, each with the user who made it, the timestamp, and the before/after values. For a test run, that includes test cases being added or removed — collapsed into a single _"N test cases added"_ / _"N test cases removed"_ entry that names the affected cases — along with results being recorded and status changes; for a session, the results and field values captured during execution. Filter by action type and date range, and open any entry for full details. Related changes made in a single save are grouped together.
47+
3648
## Tracked Actions
3749

3850
### Authentication Events

docs/docs/user-guide/permissions-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Every user has a system-wide access level that determines their baseline permiss
6060
- Create and delete projects
6161
- Assign roles within their projects
6262
- Full access to content within assigned projects
63+
- View the [audit log](./audit-logs.md#viewing-a-projects-activity) for assigned projects
6364

6465
**Use Cases**:
6566

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
title: Audit Log
3+
sidebar_position: 11
4+
---
5+
6+
# Project Audit Log
7+
8+
Every project includes an **Audit Logs** entry in the project menu. The audit log contains records of _who changed what in this project, and when._ It is the project-scoped view of the same audit trail described in the system-wide [Audit Logs](../audit-logs.md) reference.
9+
10+
## Who can view it
11+
12+
The project audit log is available to:
13+
14+
- **System administrators** (`ADMIN` access level) — for every project.
15+
- **Project Administrators** (`PROJECTADMIN` access level) — for the projects they are assigned to.
16+
17+
The menu entry appears for any Project Administrator, but it shows entries only for the projects they are assigned to. Standard users don't see the entry; they can still review [their own activity](../audit-logs.md#viewing-your-own-activity) from their profile.
18+
19+
## Opening it
20+
21+
1. Open the project.
22+
2. Choose **Audit Logs** from the project menu.
23+
24+
## What it shows
25+
26+
The view works like the system-wide audit log viewer, but every entry belongs to the current project:
27+
28+
- **Search** across entity name, user (name or email), entity type, and entity ID.
29+
- Filter by **action**, **entity type**, **user**, and **date range**.
30+
- Because every row shares one project, the **Project** column and **Project** filter are omitted.
31+
32+
For the full list of tracked actions, tracked entities, and the meaning of each detail field, see the [Audit Logs](../audit-logs.md) reference.
33+
34+
## Exporting
35+
36+
Click **Export CSV** to download the currently filtered entries for compliance reporting or external analysis. The export is itself recorded as a `DATA_EXPORTED` audit event. See [Exporting Audit Logs](../audit-logs.md#exporting-audit-logs) for the column list.

docs/docs/user-guide/projects/repository-case-details.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,16 @@ The `Forecast` value provides an automated prediction of the test case's executi
4242

4343
## Versions
4444

45-
The **Versions** tab shows the history of changes made to this test case. Each entry represents a saved version, allowing you to track modifications over time.
45+
Choose a version from the Versions menu. The **Versions** view shows the history of changes made to this test case. Each entry represents a saved version, allowing you to track modifications over time.
4646

47-
<img src="/img/screenshots/user-guide/projects/repository/case-details-versions.png" alt="Test Case Versions Tab" />
47+
<img src="/img/screenshots/user-guide/projects/repository/case-details-versions.png" alt="Test Case Versions View" />
4848

4949
* **Timestamp:** When the version was saved.
5050
* **Author:** The user who made the change.
5151
* **Changes:** A summary or link to view the specific differences between versions (details depend on implementation).
5252

53+
## Activity
54+
55+
The **Activity** button opens the case's **Activity Log** — a slide-out history of every recorded change to the test case and its fields, each with the user who made it, the timestamp, and the before/after values. Unlike the [Versions](#versions) view, which captures saved snapshots of the case content, the Activity Log reflects the full audit trail and is available to anyone who can view the case. See [Audit Logs](../audit-logs.md#viewing-an-items-activity) for details.
56+
5357
This feature is crucial for auditing and understanding the evolution of a test case. You can often revert to or compare with previous versions if needed (depending on system capabilities).

docs/docs/user-guide/projects/run-details.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,20 @@ The header displays:
2424
- **Test Run Name**: The name of the test run. In Edit mode, this becomes an editable text area.
2525
- **Action Buttons**:
2626
- **View Mode (Active Run)**:
27-
- **Edit** (`SquarePen` icon): Switches the page to Edit mode (if user has permission).
28-
- **Duplicate** (`Copy` icon): Opens the duplication dialog to create a copy of the test run.
29-
- **Export PDF** (`FileDown` icon): Exports the test run to a PDF document including all metadata, description, documentation, test cases (ordered by run order) with their execution status, results, step results, custom field values, and attachments. Available for both regular and JUnit/automated test runs.
30-
- **Complete** (`CircleCheckBig` icon): Opens a confirmation dialog to mark the run as finished. Here you select the final "Done" state from the workflow and set the completion date. This action is irreversible (if user has permission).
27+
- **Edit**: Switches the page to Edit mode (if user has permission).
28+
- **Duplicate**: Opens the duplication dialog to create a copy of the test run.
29+
- **Export PDF**: Exports the test run to a PDF document including all metadata, description, documentation, test cases (ordered by run order) with their execution status, results, step results, custom field values, and attachments. Available for both regular and JUnit/automated test runs.
30+
- **Complete**: Opens a confirmation dialog to mark the run as finished. Here you select the final "Done" state from the workflow and set the completion date. This action is irreversible (if user has permission).
3131
- **View Mode (Completed Run)**:
3232
- Displays a "Completed On [Date]" badge.
33-
- **Duplicate** (`Copy` icon): Opens the duplication dialog to create a copy of the test run.
34-
- **Export PDF** (`FileDown` icon): Exports the test run to PDF (available on completed runs as well).
35-
- **Delete** (`Trash2` icon): Opens a confirmation dialog to permanently delete the test run and all its associated results. This action is irreversible (Admin only).
33+
- **Duplicate**: Opens the duplication dialog to create a copy of the test run.
34+
- **Export PDF**: Exports the test run to PDF (available on completed runs as well).
35+
- **Delete**: Opens a confirmation dialog to permanently delete the test run and all its associated results. This action is irreversible (Admin only).
3636
- **Edit Mode**:
37-
- **Save** (`Save` icon): Saves changes made in Edit mode.
38-
- **Cancel** (`CircleSlash2` icon): Discards changes and returns to View mode.
39-
- **Delete** (`Trash2` icon): Opens a confirmation dialog to permanently delete the test run and all its associated results. This action is irreversible (Admin only).
37+
- **Save**: Saves changes made in Edit mode.
38+
- **Cancel**: Discards changes and returns to View mode.
39+
- **Delete**: Opens a confirmation dialog to permanently delete the test run and all its associated results. This action is irreversible (Admin only).
40+
- **Activity**: Available in view mode for both regular and automated runs. Opens the run's [Activity Log](../audit-logs.md#viewing-an-items-activity) — a scoped history of every change, including test cases added or removed, results recorded, and status changes. Visible to anyone who can view the run.
4041
- **Test Case Summary**: Below the title, a summary shows the progress of the test cases within the run (passed, failed, blocked, etc.).
4142

4243
## Left Panel Content

0 commit comments

Comments
 (0)