Faultsense is a lightweight (8.7 KB gzipped) browser agent that validates feature correctness in production. Your AI coding assistant instruments the assertions — the same reasoning it uses to write Playwright or Cypress tests — and real user sessions validate them.
<button
fs-assert="checkout/submit-order"
fs-trigger="click"
fs-assert-added-success=".order-confirmation"
fs-assert-added-error=".error-message[text-matches=try again]">
Place Order
</button>When a user clicks Place Order: if the order confirmation appears, the success condition passes. If an error message appears instead, the error condition passes. If neither happens, Faultsense reports a failure — which assertion, which release, what went wrong.
<script
defer
id="fs-agent"
src="https://unpkg.com/faultsense@latest/dist/faultsense-agent.min.js"
data-release-label="0.0.0"
data-collector-url="console"
data-debug="true"
/>Or initialize manually:
<script src="https://unpkg.com/faultsense@latest/dist/faultsense-agent.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
Faultsense.init({
releaseLabel: '0.0.0',
collectorURL: Faultsense.collectors.consoleCollector,
debug: true
});
});
</script>Ask your AI coding assistant to add Faultsense assertions to a component. It already knows how — same reasoning as writing E2E tests.
"Add faultsense assertions to the checkout form component"
The AI reads your component, understands what should happen when users interact with it, and generates the fs-* attributes.
Install the Faultsense skill for Claude Code:
claude plugin add Fault-Sense/faultsense-agent
Then ask Claude to instrument any component — the skill provides the full API reference and instrumentation patterns.
Every assertion needs three things:
- A key —
fs-assert="checkout/submit-order"identifies this assertion - A trigger —
fs-trigger="click"defines when the assertion activates - An expected outcome —
fs-assert-added=".success"defines what should happen
Value is a CSS selector, optionally with inline modifiers in brackets.
| Attribute | Resolves when |
|---|---|
fs-assert-added="<selector>" |
Element appears in the DOM |
fs-assert-removed="<selector>" |
Element is removed from the DOM |
fs-assert-updated="<selector>" |
Element or subtree is mutated |
fs-assert-visible="<selector>" |
Element exists and is visible |
fs-assert-hidden="<selector>" |
Element exists but is hidden |
fs-assert-loaded="<selector>" |
Media element finishes loading |
fs-assert-stable="<selector>" |
Element is NOT mutated during timeout window |
fs-assert-emitted="<event>" |
CustomEvent fires on document |
fs-assert-after="<key>" |
Parent assertion(s) have already passed |
Handle multiple outcomes from a single action using condition keys:
<button fs-assert="auth/login" fs-trigger="click"
fs-assert-added-success=".dashboard"
fs-assert-added-error=".error-msg">Login</button>First condition to match wins, others are dismissed. No server-side integration needed — the UI is the signal.
For cross-type conditionals (e.g., removed-success + added-error), use fs-assert-mutex="each" to group them.
Chained in the value using CSS-like bracket syntax:
fs-assert-updated='#count[text-matches=\d+]'
fs-assert-updated='#logo[src=/img/new.png][alt=New Logo]'
fs-assert-updated='.panel[classlist=active:true,hidden:false]'[text-matches=pattern]— Text content regex match (partial)[value-matches=pattern]— Form control.valueregex match (partial)[checked=true|false]— Checkbox/radio checked state[disabled=true|false]— Disabled state[count=N]/[count-min=N]/[count-max=N]— Element count[classlist=class:true,class:false]— Class presence check[attr=value]— Attribute check (full match)
| Trigger | When it fires |
|---|---|
click |
Element is clicked |
dblclick |
Element is double-clicked |
change |
Input value changes |
blur |
Element loses focus |
submit |
Form is submitted |
mount |
Element is added to the DOM |
unmount |
Element is removed from the DOM |
load / error |
Resource loads or fails |
invariant |
Continuous monitoring |
hover / focus / input |
Interaction events |
keydown / keydown:<key> |
Key press events |
online / offline |
Connectivity changes |
event:<name> |
Custom event on document |
Use / to group related assertions hierarchically:
fs-assert="checkout/add-to-cart"
fs-assert="checkout/submit-order"
fs-assert="profile/media/upload-photo"
Keys must be stable across releases. Human-readable labels are configured on the collector side.
| Attribute | Purpose |
|---|---|
fs-assert-timeout="<ms>" |
SLA timeout — fail if not resolved in time |
fs-assert-mpa="true" |
Persist across page navigation (MPA) |
fs-assert-mutex="<mode>" |
Cross-type conditional grouping |
fs-assert-oob="<keys>" |
Trigger on parent assertion pass (OOB) |
fs-assert-oob-fail="<keys>" |
Trigger on parent assertion fail |
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
releaseLabel |
string | Yes | — | App version or commit hash |
collectorURL |
string or function | Yes | — | Backend endpoint or custom collector function |
apiKey |
string | If URL | — | API key for the collection endpoint |
timeout |
number | No | 1000 | Default assertion timeout (ms) |
debug |
boolean | No | false | Enable console logging |
userContext |
Record<string, any> |
No | — | Arbitrary context attached to all payloads |
Each resolved assertion sends this to the collector:
interface EventPayload {
api_key: string;
assertion_key: string;
assertion_trigger: string;
assertion_type: "added" | "removed" | "updated" | "visible" | "hidden" | "loaded" | "stable" | "emitted" | "after";
assertion_type_value: string;
assertion_type_modifiers: Record<string, string>;
attempts: number[];
condition_key: string;
element_snapshot: string;
release_label: string;
status: "passed" | "failed";
timestamp: string;
user_context?: Record<string, any>;
error_context?: {
message: string;
stack?: string;
source?: string;
lineno?: number;
colno?: number;
};
}For the complete API reference including all assertion types, modifiers, OOB patterns, invariants, custom events, sequence assertions, and common patterns, see the instrumentation guide.
The fs-* attributes work in any framework that renders to the DOM.
<button onClick={handleAdd}
fs-assert="cart/add-item" fs-trigger="click"
fs-assert-updated="#cart-count">
Add to Cart
</button><template>
<button @click="handleAdd"
fs-assert="cart/add-item" fs-trigger="click"
fs-assert-updated="#cart-count">
Add to Cart
</button>
</template><button on:click={handleAdd}
fs-assert="cart/add-item" fs-trigger="click"
fs-assert-updated="#cart-count">
Add to Cart
</button>Faultsense is framework-agnostic — it observes the DOM, not framework internals — so anything that ships HTML works. The table below is verified end-to-end against real framework dev servers via Playwright on every PR. Full results, per-scenario grid, and mutation-pattern coverage live in docs/works-with.md.
| Framework | Runtime | Coverage |
|---|---|---|
| React 19 + Vite | conformance/react/ |
10/10 scenarios |
| Vue 3 + Vite | conformance/vue3/ |
10/10 scenarios |
| Svelte 5 (runes) + Vite | conformance/svelte/ |
10/10 scenarios |
| Solid 1.9 + Vite | conformance/solid/ |
10/10 scenarios |
| Alpine.js 3 | conformance/alpine/ |
10/10 scenarios |
| Astro 6 (SSR + React island) | conformance/astro/ |
11/11 scenarios (PAT-09 empirical) |
| Hotwire (Rails 8 + Turbo 8) | conformance/hotwire/ (Docker) |
8/8 scenarios (PAT-04 empirical) |
| HTMX 2 + Express | conformance/htmx/ |
7/7 scenarios |
| Livewire 3 (Laravel 11) | conformance/livewire/ (Docker) |
8/8 scenarios (PAT-04 empirical) |
| Phoenix LiveView 1.0 | conformance/liveview/ (Docker) |
8/8 scenarios (PAT-04 empirical) |
Adding a new framework means scaffolding a minimal harness and a driver file — see conformance/README.md. The Layer 1 mutation-pattern suite at tests/conformance/ locks in every DOM mutation shape the agent handles, so frameworks that produce those shapes are supported by transitivity — the Layer 2 drivers are empirical confirmation, not the source of truth.
Designed to stay off the critical path.
Benchmarked across 50-1000 assertions in a React 19 stress harness with background DOM churn and CPU throttling. All results use paired statistical comparisons with published methodology.
- 0ms INP across all configurations, including 1000 assertions under 4x CPU throttle
- Zero new long tasks — the main thread stays clean
- Sub-linear heap scaling — 140KB at 1000 assertions (less than a medium JPEG)
Published reports: performance analysis | demo benchmark | stress scaling curve
Run the benchmarks yourself:
npm run benchmark -- https://your-site.com
npm run benchmark:stressSee tools/benchmark/README.md for full methodology.
- Size: 8.7 KB gzipped
- Dependencies: None
- Browser Support: Modern browsers (ES2020+)
- Framework: Any framework that renders HTML
- License: FSL-1.1-ALv2
The examples/ directory contains reference ports you can run locally. The same assertion keys are used in each so you can diff them side-by-side and see how the instrumentation pattern works across rendering paradigms.
- todolist-tanstack — React + TanStack Router + TanStack Start (virtual DOM, JSX interpolation for dynamic assertion values).
- todolist-htmx — HTMX 2 + Express + EJS (server-rendered fragments, hx-boost SPA nav, server-side interpolation for dynamic assertion values).