UI Capture SDK errors and warnings
The Keploy capture SDK (@keploy/capture-sdk) names every error it passes to onError and every dropped-event warning it logs with a KUI_E_ code. An error's remedy.docs, and a warning's entry in the SDK's exported SDK_WARNINGS, link to the code's section on this page. Each section says what happened, why, and how to fix it. To set up UI Capture, see Capture real user flows from your web app.
| Code | Kind | Raised when |
|---|---|---|
KUI_E_INGEST_KEY_REJECTED | Error | The ingest server answers 401 or 403. |
KUI_E_INGEST_ENDPOINT_NOT_FOUND | Error | The ingest server answers 404 or 405. |
KUI_E_SESSION_CONFLICT | Error | The ingest server answers 409. |
KUI_E_INGEST_REJECTED | Error | The ingest server, or for a 413 a proxy in front of it, answers any other 4xx except 429. |
KUI_E_SESSION_ID_INVALID | Error | sessionIdGenerator does not produce an id the server would accept. |
KUI_E_DOM_RECORDER_UNAVAILABLE | Error | The SDK cannot load rrweb in the browser. |
KUI_E_PRIVACY_ROUTE_INVALID | Error | A privacy.secretRoutes or privacy.keepRoutes entry is not a valid route. |
KUI_E_EVENT_UNSERIALISABLE | Warning | An event cannot be serialised to JSON. |
KUI_E_EVENT_OVERSIZED | Warning | An event alone is over the ingest server's body cap. |
KUI_E_EVENT_OVERSIZED_SCRUBBING | Warning | Scrubbing credentials out of an event takes it over that cap. |
This page describes the SDK releases whose codes link here, all of them after 3.0.1. @keploy/capture-sdk 3.0.1 does not link here and differs in places, among them: its CaptureError.remedy is {command, manual}, with no docs and the full remedy in manual; it has no continuous option and no privacy.secretRoutes or privacy.keepRoutes; and it does not raise KUI_E_DOM_RECORDER_UNAVAILABLE, KUI_E_PRIVACY_ROUTE_INVALID or KUI_E_EVENT_OVERSIZED_SCRUBBING.
Handle SDK errors and warnings
An error ends the capture. The SDK logs it with console.error, stops recording, releases its singleton, and then calls your onError once with a CaptureError. When the ingest server refused a batch, the logged line carries the server's own explanation, and the SDK also unpatches fetch and XMLHttpRequest and drops every event it had not yet sent. A page started offline whose rrweb import then fails loses what it held the same way: see KUI_E_DOM_RECORDER_UNAVAILABLE. Because the singleton is released before onError runs, your handler can call KeployCapture.init() again to start a fresh capture. An error the SDK raises before sending anything does not reject init(): init() resolves, and the error reaches you only through onError.
One exception: with continuous: true and session ids the SDK minted, a refusal that says the server completed or filled the session ends only that session, without onError. See KUI_E_SESSION_CONFLICT and KUI_E_INGEST_REJECTED.
A CaptureError carries:
code: one of the seven error codes on this page. Branch oncode, never on the message.status: the HTTP status the server answered, ornullwhen the SDK refused before sending anything.serverMessage: theerrorfield of the server's JSON body, or the body's text, ornull.issues: the server's[{path, message}]list, which the ingest server sends only with a400to say which field it refused, ornull.remedy:{command, manual, docs}.docslinks to the code's section on this page, where the remedy is, andmanualholds the same link.commandis alwaysnull: nokeployCLI command performs these fixes.evidence: the fields listed under Evidence in the code's section below.
import {KeployCapture} from "@keploy/capture-sdk";
await KeployCapture.init({
endpoint,
apiKey,
onError: (err) => {
if (err.code === "KUI_E_INGEST_KEY_REJECTED") rotateKeyAndReload();
else report(err.code, err.serverMessage, err.evidence, err.remedy.docs);
},
});
The server's status decides the code:
| The ingest server answers | Code |
|---|---|
401 or 403 | KUI_E_INGEST_KEY_REJECTED |
404 or 405 | KUI_E_INGEST_ENDPOINT_NOT_FOUND |
409 | KUI_E_SESSION_CONFLICT |
Any other 4xx (400, 413, 415) | KUI_E_INGEST_REJECTED |
429, a 5xx, or a network failure | None: the SDK retries with backoff and never calls onError. |
2xx | None: the batch was accepted. |
A warning drops one event, and the capture continues. The SDK does not call onError. It writes one console.error line for the first such event of each code in a capture, and drops later ones in that capture silently. A new init(), or with continuous: true each new session, starts a new count. The line names the code and ends with the evidence as JSON:
[keploy] KUI_E_EVENT_OVERSIZED: dropped an event: 11059200 bytes in a body, over the ingest server's 10485760-byte cap, so no batch could carry it. The capture continues; any further such events in this capture are dropped silently. evidence={"eventId":"7","eventType":"dom","bytes":11059200,"cap":10485760}
A program that reads the console can match the code and parse the JSON after the first evidence=. Nothing before that marker comes from your page: the event's id and type are only in the JSON.
KUI_E_INGEST_KEY_REJECTED
Error. The ingest server answered 401 or 403. Capture has stopped.
What happened: The ingest server refused the SDK's API key, so no events from this page can be stored.
Cause: The key is missing, unknown, expired or revoked, its owner is deactivated, or it is not a UI ingest key. A scoped Public API key (a personal access token) is refused with 403 however it is scoped: with write scope it can mint an ingest key, but it is not one.
Fix: Mint a UI ingest key for this app and pass it as apiKey to KeployCapture.init(). You can mint one:
- In the Keploy dashboard, as described in Set up UI Capture in the dashboard.
- With
POST <api-server>/client/v1/apps/<appId>/ui/ingest-keysand a scoped Public API key withwritescope inAuthorization: Bearer, as described in Create an ingest key. The Keploy MCP server'suiCaptureSetuptool makes the same call.
Both need testgen:run on the app. A new key takes effect on the next page load.
Evidence: status, serverMessage, endpoint, sessionId, and issues when the server sent them.
KUI_E_INGEST_ENDPOINT_NOT_FOUND
Error. The ingest server answered 404 or 405. Capture has stopped.
What happened: Nothing at endpoint accepts a POST, so no events from this page can be stored.
Cause: endpoint is wrong, not the key. The SDK sends only POSTs to endpoint, and the route that accepts them is the ingest route, <api-server>/ui/v1/ingest. A 404 or 405 that reaches the SDK comes from a typo in the path under /ui/v1/, or from a same-origin proxy that does not forward the route.
Fix: Set the endpoint you pass to KeployCapture.init() to <api-server>/ui/v1/ingest (https://api.keploy.io/ui/v1/ingest on Keploy Cloud), or to a same-origin proxy that forwards that route, and reload the page.
If the endpoint is on another origin and its path is outside /ui/v1/, or the API server has no UI ingest route, the browser refuses the CORS preflight and no status reaches the SDK. The SDK retries that as a network error and never raises this code. Check the endpoint before you trust silence.
Evidence: status, serverMessage, endpoint, sessionId, and issues when the server sent them.
KUI_E_SESSION_CONFLICT
Error. The ingest server answered 409. Capture has stopped.
What happened: This session id is already claimed by a completed session or by a different capture, or a batchId was re-used with different events.
Cause: Usually a sessionIdGenerator that returns a fixed value, so a second run of the same scenario posts into the first run's session. A session completed from the dashboard while the page was still recording is refused the same way.
Fix: Give each run its own session id: remove the sessionIdGenerator override, or make it return a fresh id on every page load. Events already stored under this session are intact.
With continuous: true and session ids the SDK minted, a 409 whose body gives the reason session_completed ends only that session: the SDK does not call onError, and the next session starts at the visitor's next interaction. Any other reason, a server that sends no reason, or your own sessionIdGenerator stops the capture as above.
Evidence: status, serverMessage, endpoint, sessionId, and issues when the server sent them.
KUI_E_INGEST_REJECTED
Error. The ingest server, or for a 413 a proxy in front of it, answered a 4xx it will keep refusing: the body is malformed (400), its encoding is unsupported (415), or it is over a size limit (413). Capture has stopped.
What happened: A batch was refused, and sending it again would be refused again.
Cause: Read serverMessage: the ingest server's own sentence, or the text of a body that is not the ingest server's. A 413 from the ingest server is one of two things it does not tell apart by status:
ui session has reached its size limit: this session is complete.- A sentence containing
request body exceeds(request body exceeds N bytesfor the size on the wire,decoded request body exceeds N bytesafter gzip): the server's body cap is lower than the 10 MiB the SDK forms every batch under. The SDK and the server are at mismatched versions.
A 413 whose serverMessage holds neither sentence did not come from the ingest server. A proxy in front of it refused the body under its own limit. nginx's page says 413 Request Entity Too Large, and nginx's default client_max_body_size is 1 MiB.
A 400 or 415 is the same mismatch as request body exceeds. The SDK builds every byte it sends: it checks the session id (the one value you supply) against the server's rule before sending, clamps batchSize to the 5000-event cap it knows, and cuts every batch at the 10 MiB decoded cap. So a 4xx of this kind means the server's rules are newer than this SDK's. On a 400, evidence.issues names the field the server refused.
Fix:
- For
ui session has reached its size limit, callKeployCapture.stop()and thenKeployCapture.init()to start a new session. Withcontinuous: trueand session ids the SDK minted, a413whose body gives the reasonsession_fullneeds none of this: the SDK ends that session without callingonError, and the next one starts at the visitor's next interaction. - For a
413that holds neither sentence, raise the proxy's body limit on the ingest route to at least 10 MiB, the most the SDK sends in one request (for nginx,client_max_body_size 10m;). Upgrading the SDK changes nothing. - For a mismatch (
request body exceeds, a400or a415), upgrade@keploy/capture-sdkto the version the server documents.
Evidence: status, serverMessage, endpoint, sessionId, and issues when the server sent them.
KUI_E_SESSION_ID_INVALID
Error. Raised by the SDK before it sends anything. Capture did not start.
What happened: The configured sessionIdGenerator did not produce a session id the ingest server would accept: it returned a string outside the server's rule, returned a non-string, or threw. Nothing was sent.
Cause: evidence.received says what the generator produced: string <value>, number 42, object Map {}, a Promise (sessionIdGenerator must return a string synchronously), or threw <error>. evidence.sessionId is the string it returned, or null when it returned no string. evidence.rule is the server's rule: a session id must be 1–200 characters of letters, digits, dot, dash and underscore, with no empty dot-separated segment.
Fix: Make sessionIdGenerator return a string inside that rule, synchronously, without throwing. Encode a correlation id such as user:42/session 1 first, for example as base64url. Or remove the override and let the SDK mint a UUID. The SDK checks the id before it sends the first byte, so the fix takes effect on the next init().
Evidence: sessionId, received, rule.
KUI_E_DOM_RECORDER_UNAVAILABLE
Error. Raised in the browser before the SDK sends anything: at init(), or, for a page started offline, once the network is back and the import still fails. Capture has ended.
What happened: The SDK could not load rrweb, the library it records the page with. Without it a session holds no clicks, typing or page changes and would become no flows, so the SDK ended the capture before sending anything.
Cause: evidence.reason is the error the browser gave for the SDK's import("rrweb"). That import names the bare specifier rrweb, which a bundler (Vite, webpack, Next.js, esbuild, Rollup) resolves and a page that loads the SDK without one cannot. A Content-Security-Policy or a network failure that blocks rrweb's chunk is refused the same way; reason says which.
Fix:
-
Install
@keploy/capture-sdkwith your package manager and import it from code your bundler builds. -
With no bundler, map the specifier with an import map before the SDK loads:
<script type="importmap">
{
"imports": {
"rrweb": "<URL of rrweb's dist/rrweb.js, the version @keploy/capture-sdk depends on>"
}
}
</script> -
When a Content-Security-Policy or a network failure blocked rrweb's chunk, make the chunk reachable.
Then reload the page. A browser keeps a failed module import for the life of the page: in Chromium, a second init() after the fix makes no new request and fails again.
A page started offline does not ask for rrweb until it is back online, and sends nothing until rrweb has loaded. If the import then fails, the SDK drops what the page did meanwhile, unsent. A page that a service worker controls is different: the worker is asked for rrweb at once, and a worker that does not hold rrweb's chunk fails the import, so precache it.
Under server-side rendering there is no page, so a failed import records nothing and raises nothing.
Evidence: reason.
KUI_E_PRIVACY_ROUTE_INVALID
Error. Raised by the SDK before it sends anything. Capture did not start.
What happened: A route in privacy.secretRoutes or privacy.keepRoutes is not one the SDK can match. Recording with it would protect nothing it names, so capture did not start and nothing was sent.
Cause: evidence.option is the list the entry is in, evidence.received is the entry (string <value>, number 42), and evidence.rule is what a route must be:
/-separated segments from the start of the path, each a literal,*(any one segment) or:name(the value it names), with no?or#.- A secret route names at least one
:name, for example/invites/:keyor/*/forgotten-password/:token. - A keep route has at least one literal, for example
/teams/invite/:teamIdor/invites/settings. - The option must be an array of such strings.
Fix: Rewrite the entry inside that rule, or remove it. The SDK checks the routes before it sends the first byte, so the fix takes effect on the next init().
Evidence: option, received, rule.
KUI_E_EVENT_UNSERIALISABLE
Warning. One console.error line, for the first such event in a capture. The capture continues.
What happened: The SDK dropped one captured event because it could not be serialised to JSON: it had a cycle, a BigInt, or a getter that throws. The capture continues without it.
Cause: evidence.eventId and evidence.eventType say which event. evidence.reason is the message of the error JSON.stringify threw: its own for a cycle or a BigInt, or the getter's or toJSON's own when one throws.
Fix: A custom event comes from your code (KeployCaptureRecorder.emitEvent): make its payload plain JSON, with no cycles, no BigInt and no throwing getters. Any other event type is the SDK's own serialisation and is a defect in the SDK: report it to Keploy.
Evidence: eventId, eventType, reason.
KUI_E_EVENT_OVERSIZED
Warning. One console.error line, for the first such event in a capture. The capture continues.
What happened: The SDK dropped one captured event because, alone in a request body, it is over the ingest server's decoded cap (evidence.cap bytes), so no batch could carry it. The capture continues without it.
Cause: evidence.bytes is the size of a request body carrying only this event (its JSON plus the batch envelope), and evidence.cap is the server's limit on one body.
- A
domevent this large is an rrweb full snapshot of a very large page, or one change to it as large (a subtree inserted at once). Without it, later events would name nodes the recording never showed, so the SDK holds the page's later DOM changes and interactions until a full snapshot is delivered. It retakes one at once after a dropped change, 5 seconds after a dropped snapshot and then at doubling waits up to every 60 seconds while the page is too big, and when the tab is next shown. - A
networkevent this large is a captured body under a raisedmaxBodySize.
Fix:
- For a
domevent, leave the heaviest subtrees out of the snapshot withprivacy.blockSelectors. - For a
networkevent, lowermaxBodySizeinKeployCapture.init()so bodies are truncated before capture.
Evidence: eventId, eventType, bytes, cap.
KUI_E_EVENT_OVERSIZED_SCRUBBING
Warning. One console.error line, for the first such event in a capture. The capture continues.
What happened: The SDK dropped one captured event while scrubbing the page URL's credentials out of it. The scrubbed event was certain to pass the ingest server's decoded cap (evidence.cap bytes), so no batch could carry it, and the SDK stopped there. The capture continues without it.
Cause: The SDK scrubs an event only when the page's URL holds a credential (a declared route's value, a token), and it replaces each credential with [REDACTED]. So a short value repeated through a crafted URL makes every link rrweb resolves against that URL grow. The SDK measures the event as it scrubs it, and stops as soon as the event is certain to stay over the cap even after later replacements shrink it, rather than spend the page's time and memory on an event it could never send. evidence.atLeast is how many characters the scrubbed event was certain to reach when the SDK stopped: a lower bound on its size in a body, which is larger.
The SDK then holds the page's later DOM changes and interactions until a full snapshot is sent again. It retakes one at once after a dropped change, 5 seconds after a dropped snapshot and then at doubling waits up to every 60 seconds until one is delivered, or when the tab is next shown.
Fix: Apart from a crafted URL, a dom event this large is an rrweb full snapshot of a very large page: leave the heaviest subtrees out of the snapshot with privacy.blockSelectors.
Evidence: eventType, atLeast, cap. The event has no id, because it never reached the SDK's queue.