fix: capture XMLHttpRequest and console.error (v0.6.0) #36
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/fix-xhr-and-console-error-capture"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The SDK has three capture channels. Two were blind to any app built on axios.
ErrorBuffer)window.onerror,unhandledrejectionConsoleBuffer)log,warn,infoerrorNetworkInterceptor)globalThis.fetchXMLHttpRequestaxios uses
XMLHttpRequestand never touchesfetch, so every backend call inan axios app was invisible to the interceptor —
failedRequestscame back empty.React Query and most hand-written error handlers report through
console.error,so the console channel was empty too.
Together these are why BUG-515 in superclean-sys2 arrived with
errors: [], console: [], failedRequests: []. The reporter was notmis-transmitting evidence; it could not see any.
Fix 1 —
NetworkInterceptorpatchesXMLHttpRequestXMLHttpRequest.prototype.open/sendare now patched alongsidefetch. Bothtransports feed the same
getFailures()buffer andgetRequestLog().Decisions worth reviewing:
loadend. It is the single terminal event — it fires afterload,error,abortandtimeoutalike, so one listener covers everyoutcome rather than four that can disagree.
status === 0with noerrorevent. React Query cancels in-flight queries onunmount and axios cancels on its own; counting those as failures would bury the
real ones under routine noise. Transport failures (
error/timeout) arecounted.
[xhr network error]/[xhr timeout]). A status-0 entry with an empty body tells a reader nothing,and it is worth being able to tell a timeout from a refused connection.
responseTextis read only whenresponseTypepermits it. It throws aDOMExceptionfor any responseType other than''or'text';responseType: 'json'readsresponseand stringifies instead. Everythingelse records an empty body rather than throwing inside the patch.
always call through to the original.
destroy()restores both prototypemethods on the constructor it patched (stored, not re-read from the global).
WeakMap, so nothing is retained after the XHRis collected.
Fix 2 —
ConsoleBufferpatchesconsole.errorRather than a fourth near-identical copy-pasted block, the patched levels are now
driven by a single
PATCHED_LEVELSlist with'error'added. The bug was amissing entry in a hand-maintained set of four; one list is what stops it
recurring.
destroy()restores from a map, so the restore path can't drift fromthe patch path either.
One related fix while in there:
console.error('request failed:', err)used torecord
request failed: {}.JSON.stringify(new Error('boom'))is'{}'—an
Errorhas no enumerable own properties — so the previous formatting droppedthe only useful part of the most common error-logging call shape. Errors now
format as message + stack.
The test suite was entirely non-functional
Worth flagging separately, because it is why this shipped.
vitesthad no jsdom environment configured. Every capture module toucheswindow,documentorXMLHttpRequest, so all 31 existing tests died onReferenceError: window is not defined.pnpm testat all — thebuildjob was lint + typecheck + build.So the suite was both broken and unobserved. Fixed: added
jsdom+test.environmentinvite.config.ts, and aTeststep afterBuildinci.yml(after, becausetests/integration/build-output.test.tsasserts ondist/).Suite is now 63 passing, up from 0.
Red-green verified
26 new tests across the two modules. Reverted both source files to their
origin/devstate and re-ran: 22 of the 26 fail, then pass with the fix.They test the bug, not the implementation.
Coverage includes: 4xx and 5xx capture, request-log entries, success not
recorded as failure, transport error vs timeout vs abort, method uppercasing,
URLobjects, JSON responseType, unreadableresponseText, 1KB truncation,30-entry buffer cap, pass-through to the real
send(),destroy()restore,init()idempotence, both channels feeding one buffer, and environments with noXMLHttpRequest.Downstream — superclean-sys2 workaround can be removed
superclean-sys2 PR #423 added an axios-level workaround for exactly this
blind spot:
api-error-log.tsplus aconsole.warnmirror, so that axiosfailures would reach a channel the SDK could actually see. That workaround can
be removed once this ships — bump
@haskytech/bug-reporterto0.6.0thereand delete both.
Recommend keeping it until the version bump lands, then removing in one commit
so there is never a window with no error capture.
Scope callout
The ask was two fixes. Two things beyond it:
inert — they cannot run locally and CI would never execute them. Adding tests
to a suite that does not run would have been decoration.
ConsoleBufferloop refactor and the Error formatting fix. The firstremoves the copy-paste that caused the bug; the second is the same class of
blind spot in the channel being fixed.
Happy to split either out if you would rather review them separately.
Verification
Docs updated:
README.md(capture-channel table),docs/contracts/api-surface.yaml,docs/strategy/constraints.md,docs/strategy/posture.md,docs/constraints.md,docs/ops/key_info.md.Version bumped to 0.6.0 — additive
ConsoleMessage.levelunion widening(
'error'added) and a new exportedConsoleLeveltype.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q6VkoRihmoCUvXXYy8odKW
Two of the three capture channels were blind to any app built on axios. NetworkInterceptor patched `globalThis.fetch` only. axios uses XMLHttpRequest, so every backend call in an axios app was invisible and `failedRequests` came back empty. ConsoleBuffer patched log/warn/info but not error — React Query and most hand-written error handlers report through `console.error`, so the console channel was empty too. Together these are why BUG-515 in superclean-sys2 arrived with `errors: [], console: [], failedRequests: []` — the reporter could not see anything. NetworkInterceptor: - Patch XMLHttpRequest.prototype.open/send alongside fetch. Both transports feed the same failure buffer and request log. - Records method, url, status, duration; captures response bodies subject to the existing 1KB byte-truncation. - Finalizes on `loadend` — the single terminal event for load, error, abort and timeout alike. - Transport failures land as status 0 with a `[xhr network error]` / `[xhr timeout]` marker body, so a status-0 entry is not silently indistinguishable from an empty response. - Aborts are logged but not counted as failures. React Query and axios cancel in-flight requests routinely; counting those would bury the real failures. - `responseText` is only read when responseType permits it (it throws a DOMException otherwise); responseType 'json' reads `response` instead. - Instrumentation is wrapped so a capture failure can never break open()/send(). - destroy() restores both prototype methods on the constructor it patched. ConsoleBuffer: - Patched levels are now driven by one PATCHED_LEVELS list with 'error' added, replacing four near-identical copy-pasted blocks. A missing level was the bug; a single list is what stops it recurring. - Error arguments now format as message + stack. JSON.stringify(new Error('x')) is '{}' — an Error has no enumerable own properties, so the previous formatting dropped the only useful part of `console.error('failed:', err)`. Test infrastructure — the suite was entirely non-functional: - vitest had no jsdom environment, so all 31 existing tests died on `ReferenceError: window is not defined`, and CI never ran `pnpm test` at all. Added jsdom + `test.environment` and a Test step after Build in ci.yml. - 26 new tests for the two fixes. Verified red-green: 22 of them fail against the previous implementation. Suite is now 63 passing, up from 0. Bundle is 12.85 kB gzipped, within the 15 kB constraint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q6VkoRihmoCUvXXYy8odKW