Skip to content

Latest commit

 

History

History
58 lines (46 loc) · 5.02 KB

File metadata and controls

58 lines (46 loc) · 5.02 KB

dev-tools-observer

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.

Commands

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)

Architecture

Data flow: fetch/XHR interceptoronCapture(RawRequestData)hashersEventQueueflush()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()

Key design decisions

  • 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() calls normalizePathSegments() 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 like my-product, v2, 2024-01-15 are not replaced. The raw values are extracted into rawPathParams and hashed separately via hashRecord(), stored as pathParams on CapturedRequest.
  • Deduplication key: method + domain + normalized path + sorted query param key names (not values). /users?page=1 and /users?page=2 are the same shape; /products/123 and /products/456 are 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 to sendBatch() 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).

TypeScript gotchas

  • @tsconfig/strictest + tsup: tsup's CJS resolver can't resolve the package by name. Settings are inlined directly in tsconfig.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 let variables assigned only inside closures to null at use sites. Pattern: store in a { current: T | null } ref object, then const fn = ref.current; if (fn !== null) fn().
  • noUncheckedIndexedAccess: array/record access returns T | undefined. Use ?. and ?? defensively. Array iteration (for...of, .map()) is safe.
  • exactOptionalPropertyTypes: T | undefined is distinct from an absent optional property.

Testing patterns

  • XHR tests: Do NOT use vi.stubGlobal('XMLHttpRequest', FakeClass) — jsdom validates XHR instances internally. Instead: replace prototype methods with vi.fn() before installing the interceptor, then spy on xhr.addEventListener on the instance to capture and manually fire the readystatechange listener.
  • fetch tests: vi.stubGlobal('fetch', vi.fn(...)) works fine. Install the interceptor after stubbing so originalFetch inside the interceptor points to the stub.
  • sendBeacon: mock via vi.stubGlobal('navigator', { sendBeacon: vi.fn() }).
  • Async capture: the interceptor fires onCapture in a background void-promise. After calling window.fetch(...) in a test, await new Promise(r => setTimeout(r, 50)) before asserting on onCapture.
  • XMLHttpRequest.DONE: use the literal 4 in source code — static properties may be undefined when the prototype is replaced in tests.
  • CapturedRequest test fixtures: always include pathParams: {} — it is a required field added alongside path normalization. Tests constructing a CapturedRequest literal will fail typecheck without it.