Keep testing after the merge.
You write the assertions once, right in your markup. They keep running after you deploy, silently validated through your actual users, with no performance impact. When a flow breaks in production, Faultsense tells you which one, and who it hit.
It's E2E testing meets Real User Monitoring.
<button
fs-assert="checkout/submit-order"
fs-trigger="click"
fs-assert-added-success=".order-confirmation"
fs-assert-added-failure=".error-message">
Place Order
</button>That's the test. It lives next to the markup it checks, and runs wherever your app runs.
The assertion moves onto the UI it checks.
Before: the assertion, buried in a script you write and maintain
test('order submits', async ({ page }) => {
await page.goto('/checkout')
await page.fill('#card', '4242 4242 4242 4242')
await page.click('text=Place Order')
await expect(page.locator('.order-confirmation')).toBeVisible() // the assertion
})After: the assertion becomes an attribute
<button
fs-assert="checkout/submit-order"
fs-trigger="click"
fs-assert-added-success=".order-confirmation"
fs-assert-added-failure=".error-message">
Place Order
</button>No fixtures, no seeded data, no mocks. The state is whatever a real user actually produced. Just one question: did the right thing happen?
Presence is the simplest case. A modifier like text-matches checks the content, so you assert the right total, not just that a total showed up. Bind the expected value from the same data your component renders, and the check stays correct every render.
Before
await expect(page.locator('.order-total')).toHaveText('$149.99')After
// React — the expected total is driven by the same data you render
<span
className="order-total"
fs-assert="checkout/total-correct"
fs-assert-updated={`[text-matches=${order.total}]`}>
${order.total}
</span>text-matches is one of many. Mix and match triggers, assertion types, and modifiers to describe exactly what "correct" means for a flow. See the full attribute reference →
Works with any web framework that renders HTML
Same assertions, two drivers. In staging, point an AI agent at your flows so breaks turn up before you ship. In production, your real users drive them: no bot, no script, just the traffic you already have. The results stream to whatever stack you already run. No driver lock-in, no backend lock-in.
Add fs-* attributes to the flows that matter, right on the elements they check.
An AI agent walks your flows in staging. Your real users walk them in production.
Faultsense evaluates every annotation as the flow runs and records pass or fail.
Results stream to the stack you already run: your warehouse, your metrics pipeline, wherever you point them.
Assert what should happen. Watch it flip when it doesn't.
Interact with the list. Watch the dev-tools panel evaluate each assertion live.
(In production, pass/fail streams to whatever endpoint you point it at. The panel is just for local debugging.)
Script tag or npm.
<script src="https://cdn.faultsense.com/v0.6.0/faultsense-agent.min.js" defer></script>$ npm install @faultsense/agentThen configure it with a single Faultsense.init() call, below.
No measurable INP impact · 1.7KB idle heap · Zero long tasks · Full benchmarks →
Send pass/fail to your warehouse, your metrics pipeline, three places at once, in whatever shape your tools want. No proprietary backend, no lock-in.
Faultsense.init({
releaseLabel: 'v6.0.0',
// collectorURL takes a URL, or a function. it's your data.
collectorURL: (event) => {
datadog.logger.log('fs_assertion', event)
},
})The green synthetic check passed. Your real users still couldn't check out. That's the assertion Faultsense flips.
{
"assertion_key": "checkout/submit-order",
"assertion_trigger": "click",
"assertion_type": "added",
"assertion_type_value": ".order-confirmation",
"assertion_type_modifiers": { "timeout": "5000" },
"condition_key": "success",
"status": "failed",
"release_label": "v6.0.0",
"user_cohorts": { "plan": "pro", "region": "us-east" },
"timestamp": "2026-07-02T15:41:22.000Z"
}That's an assertion timeout, not a JS exception. The .order-confirmation your users needed never appeared within the 5s window. A pass/fail with which flow, which release, and which cohort. You own the alerting.
this worked in our lab.
this worked for Sarah on her iPhone 12 with an ad blocker on a 3G connection.
Faultsense is open source. Available on GitHub →