@enclosurejs/diagnostics
v1.1.0
Published
Application self-diagnostics for bug reports and support in Enclosure apps
Readme
@enclosurejs/diagnostics — Application self-diagnostics for bug reports and support
[!IMPORTANT] This package collects build info, platform details, lifecycle phase, capabilities, config, settings, and extension data into structured snapshots and health reports. Sensitive keys are automatically redacted. The output is designed for pasting into support tickets or rendering in a built-in diagnostics panel widget.
The Problem
When users report bugs in desktop apps, support teams need to know: which version, which platform, what config, what's healthy and what's broken. Manually gathering this information is tedious and error-prone — users forget details, developers hardcode console.log dumps, and sensitive data leaks into bug reports.
@enclosurejs/diagnostics solves this by providing a single DiagnosticsService that aggregates everything the DI context knows — build stamp, backend platform, lifecycle phase, capabilities, config, settings — into a structured snapshot with automatic redaction of sensitive keys. Extensions can contribute their own sections via DiagnosticProvider.
Architecture
┌──────────────────────────────────────────────────────────┐
│ DiagnosticsService │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ snapshot() │ │ health() │ │ toReport() │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ┌──────▼────────────────▼───────────────────▼─────────┐ │
│ │ DI Context (reads tokens): │ │
│ │ BackendToken · LifecycleToken · ConfigToken │ │
│ │ SettingsToken · SystemInfoToken · capabilities() │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DiagnosticProvider[] (extension data + health) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ redact(obj, patterns) — glob-based key redaction │ │
│ └─────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────┤
│ Optional: WidgetRegistry panel (diagnostics-panel slot) │
└──────────────────────────────────────────────────────────┘Dependency rule: diagnostics imports core, and optionally config + settings (peer dependencies).
Quick Start
import { createApp } from '@enclosurejs/core';
import { createConfigModule } from '@enclosurejs/config';
import { createLoggingModule } from '@enclosurejs/logging';
import { createDiagnosticsModule, DiagnosticsToken } from '@enclosurejs/diagnostics';
const app = createApp({
modules: [
createConfigModule({ app: 'MyApp', version: '2.0.0' }),
createLoggingModule(),
createDiagnosticsModule({
widget: true, // register diagnostics panel in WidgetRegistry
redactPatterns: ['*password*', '*secret*', '*token*', '*key*', 'email'],
}),
],
});
await app.start();
// Collect a snapshot
const diag = app.context.use(DiagnosticsToken);
const snap = diag.snapshot();
console.warn(snap.build.version); // '2.0.0'
// Run health checks
const report = await diag.health();
console.warn(report.status); // 'ok' | 'warn' | 'fail'
// Human-readable text for support tickets
const text = diag.toReport();How It Works
Snapshot Collection
snapshot() reads from DI tokens that are already bound by other modules:
- Build info —
ConfigTokenprovidesapp,version,mode,revision,dirty,tag - Platform —
BackendToken.platform+navigator.userAgent+process.versions.node - Capabilities —
context.capabilities()sorted alphabetically - Lifecycle —
LifecycleToken.phase - Config — full config object, redacted
- Settings —
SettingsToken.current, redacted (omitted when not bound) - Extensions — each
DiagnosticProvider.collect()result keyed by provider name
All tokens are resolved via tryUse() — missing tokens produce 'unknown' values, never throw.
Redaction
redact(obj, patterns?) deep-clones an object and replaces values whose key matches any glob pattern with '[REDACTED]'. Default patterns: *password*, *secret*, *token*, *key*. Patterns are case-insensitive. * matches any substring.
Health Checks
health() runs built-in checks (lifecycle phase, config availability, system info) plus all provider health checks. Status propagates worst-wins: fail > warn > ok.
Widget
When widget: true and WidgetRegistryToken is bound, a __diagnostics-panel widget is registered in the diagnostics-panel slot. It renders health status, build info, platform, capabilities, config, settings, and extensions with a Refresh button and a Copy Report button.
API
| Export | Kind | Purpose |
| --------------------------- | --------- | ------------------------------------------------------------------- |
| DiagnosticsService | interface | Snapshot, health check, and text report contract |
| DiagnosticsServiceImpl | class | Implementation that reads from DI context + providers |
| DiagnosticsToken | token | Resolve DiagnosticsService from DI |
| createDiagnosticsModule | factory | Creates a Module that wires diagnostics into the app |
| redact(obj, patterns?) | fn | Deep-clone with glob-based key redaction |
| registerDiagnosticsWidget | fn | Registers the diagnostics panel widget in a WidgetRegistry |
| DiagnosticsOptions | type | Options for createDiagnosticsModule (providers, patterns, widget) |
| DiagnosticSnapshot | type | Structured snapshot shape (build, platform, capabilities, ...) |
| HealthReport | type | { status, checks } — aggregated health result |
| HealthCheck | type | { name, status, message? } — individual check result |
| DiagnosticProvider | type | Extension point for modules/plugins to contribute diagnostics data |
| DiagnosticsWidgetOptions | type | Widget slot and order overrides |
Configuration
createDiagnosticsModule accepts DiagnosticsOptions:
| Option | Type | Default | Description |
| ---------------- | ------------------------------- | ------------------------------------------------ | -------------------------------------------- |
| redactPatterns | readonly string[] | ['*password*', '*secret*', '*token*', '*key*'] | Glob patterns for sensitive key redaction |
| providers | readonly DiagnosticProvider[] | [] | Initial extension providers |
| widget | boolean | false | Register diagnostics panel in WidgetRegistry |
Types Exported
| Type | Used by |
| ------------------------------ | --------------------------------- |
| DiagnosticsService | any code consuming diagnostics |
| DiagnosticSnapshot | snapshot consumers |
| HealthReport / HealthCheck | health check consumers |
| DiagnosticProvider | modules/plugins contributing data |
| DiagnosticsOptions | module creation |
| DiagnosticsWidgetOptions | widget registration |
Safety
Type Safety
- All snapshot fields have explicit readonly types — no
anyin the public surface. DiagnosticProvider.collect()returnsRecord<string, unknown>— callers own the shape.HealthCheck.statusis a string union ('ok' | 'warn' | 'fail'), not an open string.
Error Safety
- All DI tokens are resolved via
tryUse()— missing tokens produce'unknown', never throw. SystemInfoServicecalls inhealth()are wrapped in try/catch — failures degrade to'warn'.redact()deep-clones — the original object is never mutated.
Runtime Safety
- Module declares
requires: ['config', 'logging']— won't install before its dependencies. - Widget registration is guarded by
tryUse(WidgetRegistryToken)— no crash when no frontend.
Benchmarks
No benchmarks — diagnostics is not a hot path. Snapshot collection and health checks run on-demand (user clicks Refresh or pastes a report). The redact() function is O(n) in the size of the config object.
Bundle Size
| Output | File | Size |
| ------------ | ------------ | -------- |
| Runtime (JS) | index.js | 13.03 KB |
| Types (DTS) | index.d.ts | 4.09 KB |
| Total | | 17.12 KB |
External dependencies (@enclosurejs/core, @enclosurejs/config, @enclosurejs/settings) are not bundled — they are peer dependencies.
Quality
| Metric | Value |
| --------------------- | ------------------------------------------------------------------------------------ |
| Unit tests | 51 (all pass) |
| Test files | 3 (service.test.ts, redact.test.ts, module.test.ts) |
| Source files | 6 (index.ts, types.ts, service.ts, redact.ts, module.ts, widget.ts) |
| External dependencies | 0 |
| Peer dependencies | @enclosurejs/core (required), @enclosurejs/config + @enclosurejs/settings (optional) |
| Coverage thresholds | statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90% |
Quality Layers
Layer 1: STATIC ANALYSIS (every commit)
tsc --noEmit strict mode, zero errors
eslint ESLint 9 flat config, zero warnings
prettier --check formatting
Layer 2: UNIT TESTS (every commit)
51 tests snapshot, health checks, toReport, providers,
redaction patterns, DI integration, widget registration
v8 coverage statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90%
Layer 3: BENCHMARKS
N/A on-demand diagnostics — no hot path
Layer 4: PACKAGE HEALTH
0 external deps pure TypeScript + @enclosurejs/core
tsup build ESM + DTS output, single entrypointFile Structure
packages/diagnostics/
├── src/
│ ├── index.ts Barrel: all public exports
│ ├── types.ts DiagnosticsService, DiagnosticSnapshot, HealthReport, DiagnosticProvider
│ ├── service.ts DiagnosticsServiceImpl — aggregates DI tokens into snapshots
│ ├── redact.ts redact(obj, patterns?) — glob-based key redaction
│ ├── module.ts createDiagnosticsModule, DiagnosticsToken
│ ├── widget.ts registerDiagnosticsWidget — WidgetRegistry panel
│ └── __tests__/
│ ├── service.test.ts 25 tests — snapshot, health, toReport, providers, system-info
│ ├── redact.test.ts 13 tests — patterns, nesting, arrays, edge cases
│ └── module.test.ts 13 tests — module wiring, options, widget registration
├── package.json
├── tsconfig.json
└── tsup.config.tsLicense
MIT
