Audit-Grade Observability
The observability substrate captures every state change, API call, and navigation as a structured trace entry. Audit-grade observability turns that stream into a compliance-ready audit trail: it redacts PII, enforces sampling and retention, correlates work across navigations and fetches with W3C Trace Context, and forwards everything to an OTLP/JSON backend (or to a sink you register yourself).
It is the same trace stream that drives the Inspector in development. You don't run two pipelines.
What problems this prevents
- A trace shipped off-browser can no longer carry plaintext passwords,
tokens, credit-card numbers, or
Authorizationheaders — the redactor drops or masks them before the entry leaves the page. - Backend logs no longer disagree with the browser timeline: every
outgoing fetch carries a
traceparentheader that ties the server-side span back to the browser entry that started it. - Production volume no longer drowns out signal — head sampling decides per-trace whether to keep all entries, and tail sampling holds entries in memory just long enough to flush them only when an error fires.
- A misconfigured redaction policy is no longer silent: the audit
pipeline emits its own self-diagnostics (
audit-redaction-missing,audit-policy-conflict,audit-sink-failure,audit-pii-leaked,audit-correlation-missing,audit-buffer-overflow) so you can see, and in strict mode block, leaks before they ship.
How it works
Every internal pushXsLog() call passes the resulting entry through the
audit pipeline:
- Redaction — runs the entry's leaf paths against your declared
RedactionRule[](dot-paths with*and**wildcards). Conflicts resolve by aggressiveness:drop > hash > mask. - Content-PII scan — if a string slipped through the structural
rules but matches a built-in heuristic (email, JWT, credit-card, SSN,
API-key, IPv4, phone) or one you registered, the pipeline emits
audit-redaction-missing. With strict mode on, the entry is also dropped andaudit-pii-leakedrecords the leak. - Sampling — applies the configured head and tail sampling rules.
- Sink delivery — forwards surviving entries to the OTLP/JSON sink, the console sink, or a custom sink you registered by name.
Framework-internal entries (versioning, forms, audit, build,
errors, lifecycle, sandbox:warn, log:*, i18n) bypass redaction
and PII scanning by design — their payloads are framework-produced
diagnostic strings, never user data.
Declaring a policy
Add an auditPolicy to your <App> (object literal):
<App
auditPolicy="{{
redact: [
{ selector: 'headers.Authorization', mode: 'drop' },
{ selector: 'payload.email', mode: 'hash' },
{ selector: '**.password', mode: 'mask' }
],
sample: { head: { rate: 0.1 }, tail: { keepIfErrorIn: ['fetch:fail'] } },
retention: { bufferSize: 500, onOverflow: 'drop-oldest' },
sink: { kind: 'otlp', endpoint: 'https://otel.example.com/v1/traces' }
}}"
/>config.json also accepts the same shape under
xmluiConfig.auditPolicy. The framework normalises both into the same
internal representation.
Strict mode (on by default)
Strict audit logging is enabled by default. In strict mode:
audit-redaction-missingandaudit-policy-conflictare reported as errors instead of warnings.- An entry that fails PII scanning is dropped and recorded as
audit-pii-leaked, so it never reaches any sink.
To downgrade to warn-only mode during migration, set
strictAuditLogging: false in config.json:
{
"xmluiConfig": {
"strictAuditLogging": false,
"auditPolicy": { /* ...as above... */ }
}
}Tip: Start with
strictAuditLogging: falseto surfaceaudit-redaction-missingwarnings without dropping entries, add the needed redaction rules, then remove the override to restore strict mode.
Registering a custom sink
When a built-in transport doesn't fit, register a sink factory and reference it from policy by name:
<App onReady="App.registerAuditSink('my-collector', cfg => ({
push: entry => fetch(cfg.endpoint ?? '/audit', {
method: 'POST',
body: JSON.stringify(entry)
}),
flush: () => Promise.resolve()
}))">
...
</App>Then declare it:
auditPolicy="{{
sink: { kind: 'custom', endpoint: 'my-collector' }
}}"The string in endpoint is the name passed to registerAuditSink.
Registering a custom PII heuristic
<App onReady="App.registerAuditHeuristic('internal-id', /^INT-[0-9]{6}$/)" />Values that match the pattern will fire audit-redaction-missing unless
a redaction rule already covers their path.
Correlation with backend traces
When a fetch leaves the browser via the framework's HTTP layer, the
pipeline injects a traceparent header in W3C Trace Context format. The
server-side span derives its traceId from the same value, so the
end-to-end trace can be assembled from browser + backend logs without
any custom plumbing.
Filtering the Inspector
The Inspector toolbar exposes one-click filter pills above the
trace stream — audit, versioning, lifecycle, errors, log:warn,
log:info. Clicking a pill hides every event whose kind starts with
that token; clicking again restores it. Use them to silence noisy
internal diagnostic kinds while you focus on application traces, or to
isolate just the audit-pipeline diagnostics during a redaction review.