You tested it in the lab.
Your users run it on their device.

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.

Your assertion, lifted out of the script.

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?

Assert the value, not just the element.

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

Faultsense is the assertion layer.
It doesn't care what's holding the mouse.

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.

How it works

01

Annotate

Add fs-* attributes to the flows that matter, right on the elements they check.

02

Drive

An AI agent walks your flows in staging. Your real users walk them in production.

03

Assert

Faultsense evaluates every annotation as the flow runs and records pass or fail.

04

Report

Results stream to the stack you already run: your warehouse, your metrics pipeline, wherever you point them.

Demo

Assert what should happen. Watch it flip when it doesn't.

Tasks v6.0.0
Review pull request #847
Deploy v6.0.0 to staging
Update API documentation

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.)

Install

Script tag or npm.

<script src="https://cdn.faultsense.com/v0.6.0/faultsense-agent.min.js" defer></script>
$ npm install @faultsense/agent

Then configure it with a single Faultsense.init() call, below.

No measurable INP impact · 1.7KB idle heap · Zero long tasks · Full benchmarks →

Open source. You own the data.

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)
  },
})

When a flow breaks in the field, you get a signal you can act on.

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 →