Zero-dependency TypeScript browser library that intercepts all HTTP requests (fetch + XHR), hashes sensitive values, deduplicates similar requests, and batches structured data to a configurable reporting server. Goal: automatically map API endpoint usage to enable automated end-to-end test generation.
npm test # run all tests (vitest, jsdom environment)
npm run test:watch # watch mode
npm run test:coverage # coverage via v8
npm run typecheck # tsc --noEmit (strict mode, zero errors required)
npm run build # tsup → dist/ (ESM + CJS + IIFE)Data flow: fetch/XHR interceptor → onCapture(RawRequestData) → hashers → EventQueue → flush() → sendBatch() → sendBeacon/fetch
src/
index.ts # public re-exports
sdk.ts # ObserverSDK static class (init/flush/destroy)
types.ts # all interfaces (SDKConfig, RawRequestData, CapturedRequest, HashedBody, etc.)
blocklist.ts # analytics domain blocklist + isBlocked()
hasher.ts # SHA-256, space-split hashing, JSON tree walker
interceptor-fetch.ts # window.fetch monkey-patch
interceptor-xhr.ts # XMLHttpRequest.prototype monkey-patch
queue.ts # EventQueue with deduplication
sender.ts # sendBeacon + fetch fallback, gzip via CompressionStream
url-parser.ts # URL decomposition, isIdSegment(), normalizePathSegments(), makeDedupeKey()
- Space-split hashing: Header values are split on spaces before hashing —
"Bearer eyJ..."→"<hash(Bearer)> <hash(eyJ...)>". Preserves structural shape (2-part vs 1-part) without revealing values. - Path parameter normalization:
parseUrl()callsnormalizePathSegments()which replaces ID-looking segments with:param0,:param1, etc. Detected patterns: pure numeric, UUID, MongoDB ObjectId, hex ≥8 chars, ULID, base64url (length ≥8,[A-Za-z0-9_-], contains both uppercase letter AND digit). Human slugs likemy-product,v2,2024-01-15are not replaced. The raw values are extracted intorawPathParamsand hashed separately viahashRecord(), stored aspathParamsonCapturedRequest. - Deduplication key: method + domain + normalized path + sorted query param key names (not values).
/users?page=1and/users?page=2are the same shape;/products/123and/products/456are also the same shape (both normalize to/products/:param0). - Anti-recursion:
isBlocked()also blocks the SDK's own reporting endpoint to prevent infinite loops. - originalFetch: saved at
init()time before the interceptor is installed; passed tosendBatch()so reporting calls bypass the interceptor. - Response stream: fetch interceptor clones the response (
response.clone()) before reading the body — the original stream is returned to the caller untouched. - gzip:
CompressionStream('gzip')with graceful fallback to plain JSON when unavailable (e.g., older browsers or test environments).
@tsconfig/strictest+ tsup: tsup's CJS resolver can't resolve the package by name. Settings are inlined directly intsconfig.json(do not use"extends": "@tsconfig/strictest").- Header normalization: all captured header keys are lowercased (
key.toLowerCase()). Always access them lowercase:raw.requestHeaders['content-type'], not'Content-Type'. - Closure narrowing with strict settings: TypeScript narrows
letvariables assigned only inside closures tonullat use sites. Pattern: store in a{ current: T | null }ref object, thenconst fn = ref.current; if (fn !== null) fn(). noUncheckedIndexedAccess: array/record access returnsT | undefined. Use?.and??defensively. Array iteration (for...of,.map()) is safe.exactOptionalPropertyTypes:T | undefinedis distinct from an absent optional property.
- XHR tests: Do NOT use
vi.stubGlobal('XMLHttpRequest', FakeClass)— jsdom validates XHR instances internally. Instead: replace prototype methods withvi.fn()before installing the interceptor, then spy onxhr.addEventListeneron the instance to capture and manually fire thereadystatechangelistener. - fetch tests:
vi.stubGlobal('fetch', vi.fn(...))works fine. Install the interceptor after stubbing sooriginalFetchinside the interceptor points to the stub. sendBeacon: mock viavi.stubGlobal('navigator', { sendBeacon: vi.fn() }).- Async capture: the interceptor fires
onCapturein a background void-promise. After callingwindow.fetch(...)in a test,await new Promise(r => setTimeout(r, 50))before asserting ononCapture. XMLHttpRequest.DONE: use the literal4in source code — static properties may be undefined when the prototype is replaced in tests.CapturedRequesttest fixtures: always includepathParams: {}— it is a required field added alongside path normalization. Tests constructing aCapturedRequestliteral will fail typecheck without it.