Install
In your project, open Settings, Production monitoring and create an ingest key. The page shows this snippet with your key filled in, a button that sends a test event, and waits until your first real event arrives.
<script src="https://getchecked.twelveai.app/sdk/checked.v1.min.js"
data-key="ck_pub_..."
data-env="production"
data-release="1.4.2"></script>Script tagPut it in the head of every page, before your own scripts. It is under 10 KB gzipped, has no dependencies, and sends nothing unless an error, a failed request or a slow page happens, or you call track.
React, Next.js, Vue and other bundlers
npm install @checked/browser
import { Checked } from "@checked/browser";
Checked.init({ key: "ck_pub_...", environment: "production", release: "1.4.2" });
Checked.identify({ id: user.id }); // optional, after sign-in
Checked.captureError(error); // errors you catch yourself
Checked.track("checkout_completed", { plan: "pro" });npmIn Vue, also pass render errors on: app.config.errorHandler = (err) => Checked.captureError(err). In Next.js, call init once from a client component in the root layout.
Node.js servers
npm install @checked/node
import { Checked, expressErrorHandler } from "@checked/node";
Checked.init({ key: "ck_pub_...", environment: "production", release: process.env.RELEASE });
app.use(expressErrorHandler()); // after your routesNodeThe Node SDK captures uncaught exceptions and unhandled rejections, and ships helpers for Express, Fastify and AdonisJS error handlers. 4xx errors are skipped unless you ask for them.
What it captures
- Errors: uncaught errors and unhandled promise rejections with stack traces, plus anything you pass to captureError.
- Failed requests: fetch and XMLHttpRequest calls that return 5xx or fail to connect, with method, URL, status and duration. Never bodies or headers.
- Slow pages: poor Largest Contentful Paint, Interaction to Next Paint, Cumulative Layout Shift, server response time and load time.
- Breadcrumbs: the last 30 clicks, page changes (including single-page app route changes) and requests before each error.
- Context: release, environment, page URL, browser, operating system, device type and screen size, an anonymous visitor id, and the user id if you call identify.
- Key actions: Checked.track(name, props) for the moments you care about, like a completed checkout.
Options
Pass these to Checked.init, or as data- attributes on the script tag (data-env, data-release, data-sample-rate, data-console-errors, data-capture-4xx).
| Option | Default | What it does |
|---|---|---|
| key | required | The project’s public ingest key (ck_pub_...). It can only send events, so it is safe in page source. |
| environment | production | Keeps staging and production apart in the inbox. |
| release | none | Your app version. Powers “first seen in” and regression detection. |
| sampleRate | 1 | 0 to 1, applied to errors, failed requests and slow pages. Custom events are never sampled. |
| captureConsoleErrors | false | Also report console.error calls. |
| captureRequests4xx | false | Also report 4xx responses. 5xx and network failures are always reported. |
| captureQueryStrings | false | Keep ?query and #hash in URLs. Off by default because they often carry tokens. |
| replay | off | Session replay: { onErrorSampleRate, sampleRate, maskAllText, maskSelector, blockSelector, maxDurationMinutes }. On the script tag: data-replay="true" plus data-replay-error-sample-rate, data-replay-sample-rate, data-replay-mask-all-text, data-replay-mask, data-replay-block. |
| respectDoNotTrack | false | When on, browsers with Do Not Track or Global Privacy Control send nothing. |
| sendEmail | false | identify() hashes the email in the browser. Turn this on only if you want the address itself stored. |
| beforeSend | none | A function that receives each event last. Return it changed, or null to drop it. |
| reportAllVitals | false | Report every Web Vital. By default only poor ones are sent. |
Privacy
- Input values are never read. A click on a field is recorded as the field, like input[name=email], not what was typed. Session replay masks every input in the browser too.
- Query strings and fragments are stripped from every URL, including URLs inside error messages and stack traces.
- Request and response bodies and headers are never captured.
- Anything that looks like a bearer token, a password=, token= or api_key= value, a JWT or a card number is removed on our servers too, as a second line of defence.
- Emails passed to identify are hashed in the browser unless you turn on sendEmail.
- beforeSend lets you scrub or drop any event before it leaves the page, and sampleRate and respectDoNotTrack reduce what is sent.
- Occurrences are deleted after your plan’s retention period. The issue itself, its status and comments stay.
You are the controller of the data your app sends. If your users are in a region that requires consent for error monitoring, load the script after consent.
Session replay
Replay shows what the person saw and did in the seconds before an error: the page, their clicks and scrolling, page changes and failed requests, next to the issue. It is off until you turn it on, and it loads as a separate script, so pages without it never download the recorder.
<script src="https://getchecked.twelveai.app/sdk/checked.v1.min.js"
data-key="ck_pub_..."
data-replay="true"
data-replay-error-sample-rate="1"
data-replay-sample-rate="0"></script>Script tagChecked.init({
key: "ck_pub_...",
replay: {
onErrorSampleRate: 1, // keep the last minute, upload it when an error happens
sampleRate: 0, // fraction of sessions recorded from the start
maskAllText: false, // inputs are always masked; this masks all text too
blockSelector: ".card-form",
load: () => import("@checked/browser/replay"),
},
});npm- On error (the default): the browser keeps the last 30 to 60 seconds in memory and uploads nothing unless an error or failed request is captured. Then it uploads that minute and keeps recording the rest of the visit.
- Whole sessions: sampleRate records that fraction of sessions from the start.
- Masked by default: every input, textarea and dropdown is masked in the browser before anything is recorded. Passwords show a fixed mask, so not even their length is kept.
- Your markers: add
data-checked-maskto hide text inside an element, anddata-checked-blockto leave an element out entirely (a grey box of the same size is shown). maskAllText masks every piece of text on the page. - Never recorded: canvas, cross-origin iframes, query strings in page and link addresses, typed values, and anything when Do Not Track or Global Privacy Control is on.
- Who can watch: members of your workspace only. Replays have no public links and are deleted after your plan’s replay retention.
Each plan includes a monthly number of replays, a retention period and a maximum length and size per replay. Past them the recorder stops quietly; your app never sees an error. If your site sets a Content-Security-Policy, allow the Check’d origin in script-src and connect-src.
Issues and grouping
Occurrences of the same problem become one issue, so ten thousand identical errors are one line in your inbox:
- Errors group by error type, the message with numbers, ids and URLs taken out, and the top frames of your own code (function and file, ignoring line numbers and build hashes).
- Failed requests group by method, host, route with ids collapsed (/orders/:id) and status class (5xx, 4xx, network).
- Slow pages group by page route and metric.
Each issue has a status (new, investigating, resolved, ignored, regressed), an owner, comments, the browsers, systems and releases it affects, and a 30 day chart. Test findings from runs, website scans and load tests sit in the same inbox, so a bug your tests found and the same bug hitting users are side by side.
Minified code without source maps groups less precisely, because function names change between builds. Source map support is planned.
Alerts go to the email addresses and Slack channel in your project’s notification settings when a new issue appears, a resolved one comes back, or an issue spikes past a threshold you set.
Releases
Set release on the SDK and Check’d records the first release each issue appeared in. When you resolve an issue you can say which release fixed it. If it shows up again in a newer release, it is marked regressed and you get an alert. Errors from older releases still open in someone’s tab do not count.
To record deploys from CI, call the releases API with a workspace API key:
curl -X POST https://getchecked.twelveai.app/api/v1/releases \
-H "Authorization: Bearer $CHECKED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"projectId": "shop-kqzxm", "version": "1.4.2", "environment": "production", "commitSha": "abc123"}'Register a releaseLimits
Each plan includes a monthly number of events, a retention period and a per-key rate. Past the monthly allowance, events are dropped and counted on your Plan and usage page, and your app never sees an error. Monitoring allowances are early and may change; current numbers are shown in the app.
Ingest API
The SDKs use this endpoint. You can call it yourself from any language. Send up to 50 events per request and keep bodies under 256 KB. Sending JSON as text/plain avoids a CORS preflight in browsers.
POST https://getchecked.twelveai.app/api/v1/ingest/events?key=ck_pub_...
Content-Type: text/plain;charset=UTF-8
{
"key": "ck_pub_...",
"sdk": { "name": "checked.browser", "version": "1.0.0" },
"sentAt": 1760000000000,
"events": [
{
"type": "error",
"ts": 1760000000000,
"environment": "production",
"release": "1.4.2",
"url": "https://app.example.com/orders/123",
"user": { "anonymousId": "a_9f2c" },
"error": { "type": "TypeError", "message": "...", "stack": "...", "handled": false, "mechanism": "onerror" }
}
]
}Ingest request| 202 | Accepted. The body says how many were accepted and dropped. |
| 200 | Over this month’s allowance. Dropped and counted; do not retry. |
| 400 | The body is not a batch of events. |
| 401 | Unknown or revoked key. |
| 403 | The page’s origin is not on the key’s allowed list. |
| 413 | The body is too large. |
| 429 | Too many events for this key right now. Retry after the Retry-After seconds. |
Questions? Contact support.