Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5d4a2d4
fix(replay): keep a replay base in the capture window; shed replay be…
krassx Oct 31, 2026
90a09ca
docs(design): streaming report export (Android pattern, window-bounde…
krassx Oct 31, 2026
d5f3c73
docs(design): streaming export S0 baseline results
krassx Oct 31, 2026
b1eff81
feat(core): BundleBody contract and in-memory body (streaming export S1)
krassx Oct 31, 2026
9f87d4d
feat(core): streaming bundle assembler (streaming export S2)
krassx Oct 31, 2026
0e612b3
feat(core): time plan for streaming export (S3)
krassx Oct 31, 2026
e70d560
feat(replay): streaming replay encoder with orphan trim and window cl…
krassx Oct 31, 2026
5bc731b
feat(core,node-utils,browser-utils): bundle sinks and body-aware tran…
krassx Oct 31, 2026
83f8d38
feat(core,node-utils): streamed durable staging of file-backed bundle…
krassx Oct 31, 2026
4478373
feat(core,node,browser): report assembly streams end to end (streamin…
krassx Oct 31, 2026
d939c61
feat(browser): raise the default capture byte cap to 20 MB (30 MB wit…
krassx Oct 31, 2026
1b94667
feat(core): recovery paths stream their rebuilt bundles (streaming ex…
krassx Nov 1, 2026
b80910c
test(instrumentation): the retired-event-name guard skips gitignored …
krassx Nov 1, 2026
25bf470
feat(electron,core,node,browser): main tells renderers to restart the…
krassx Nov 1, 2026
fefa2d2
fix(core,electron): recovery reports torn records to its own sink; ma…
krassx Nov 1, 2026
dc5de12
perf(browser-utils): the IndexedDB snapshot streams part by part, so …
krassx Nov 1, 2026
bfb2837
feat(core,browser-utils): one visual lane and one replay member per s…
krassx Nov 1, 2026
a7f50ac
feat(electron,node): each window's replay is its own lane in main; a …
krassx Nov 1, 2026
9222fed
fix(core,browser-utils): a laned member keeps the type's manifest att…
krassx Nov 1, 2026
41c901e
feat(core,browser-utils): the window lane is persisted in the part me…
krassx Nov 1, 2026
ceff29d
feat(core,electron): a closed window's replay lane is retired once it…
krassx Nov 1, 2026
33975b5
docs(streaming-export): the browser export residual is the window's l…
krassx Nov 1, 2026
a5469db
feat(core,electron): pixel video per display: a member per lane for e…
krassx Nov 1, 2026
920c803
fix(core,electron): display video keeps one ring per physical display…
krassx Nov 1, 2026
b631a59
test(instrumentation): raise the webview bundle budget to 204 KiB raw…
krassx Nov 1, 2026
adc0fba
test(core): give the two heavy sink back-pressure tests a 120 s limit…
krassx Nov 1, 2026
4839694
fix(core,node,electron,replay): main builds replay.bin through the re…
krassx Nov 1, 2026
f9e851f
test(bugsee): classify the new streamEncoders launch option as host-s…
krassx Nov 1, 2026
8861841
fix(core,electron,node): a relaunch keeps the host's stream encoders;…
krassx Nov 1, 2026
286172d
feat(electron): launchMain hands a video.source factory the tracker's…
krassx Nov 2, 2026
9251a5c
fix(electron): a throwing video.source factory is reported and launch…
krassx Nov 2, 2026
5a94293
fix(electron): a failed window keeps an opaque placeholder in the dis…
krassx Nov 2, 2026
e883b26
fix(electron): skip grabbing a display's windows while its earlier co…
krassx Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Read this first; then `docs/design/sdk-design.md` (Draft v3) for the full archit
- **Mobile-parity options (T6, `docs/design/mobile-api-alignment.md`):** `captureLogsLevel`, `captureBreadcrumbs` (JS default stays `true`), `detectHttpErrors` (`createHttpErrorDetectionProvider` in `@bugsee/capture`, mechanism `http-error`, own rate budget), `captureNetworkDefaultSanitizer`, `reportDefaultCrashPriority` / `reportDefaultErrorPriority` (applied in the client before the report handler, only to severities that were derived), and `getDefaultNetworkSanitizer()` for composing with a custom network filter.
- **`client.notify(title, body?, severity?, fields?, urgent?)`** (T7, `docs/design/mobile-api-alignment.md` "T7 findings"): relays a message to the app's Slack / webhook integrations through the existing collector route `POST /v2/notifications` (app token in the body, no session, public CORS, no app-type check; verified in the appserver source). Core `createNotifyRelay` (`notify-relay.ts`): in-memory queue, batches of 50 / 256 KB, `notifyFlushDelay` window (default 0), `urgent` skips it, 2 s to 60 s backoff on transient failure, per-item `failed` retried; `BugseeApi.notify` classifies the answer; `flush()`/`stop()` deliver. Wired in browser and node. **Not persisted** (mobile persists to disk); Vercel edge / web worker / Cloudflare / WebView have no sender yet.
- **Value-export parity of directly imported packages (review round 2, R2D-4):** `@bugsee/vercel-edge`, `cloudflare`, `webworker`, `webview`, `browser`, `node`, `electron` (`/main`, `/renderer`) and every framework-adapter subpath (`nextjs`/`remix`/`sveltekit`/`astro`/`nuxt` `/server`, `/edge`, `/client`) re-export the umbrella's value API (`BugseeStatus`, `LifecycleEvents`, `getStatus`, `addReportAttachment`, `getDefaultNetworkSanitizer`, `relaunch`) as thin re-exports of the same functions. `relaunch` is built per package on core `relaunchWith` so it re-runs THAT package's launch (cloudflare, bun, deno, webworker, vercel-edge, `@bugsee/nextjs/server` -> `registerServer`; the OTel provider slot is first-wins so nothing is re-attached on relaunch: the span processor registered at first launch is a stable late-bound delegate that a relaunch re-points at the new client, R3P-2; `stop()` without a relaunch drops its target and no delegate exists when OTel is off, R4H-9). The sveltekit/astro/nuxt `/edge` entries are the exception to "thin re-export" for `relaunch`: each defines its own that re-runs the launch seam remembered on the carrier at install (`rememberEdgeLaunch`/`relaunchEdge` in `@bugsee/vercel-edge`; an explicit `launch` argument wins, R4H-4). Deliberately absent: `relaunch` on `@bugsee/electron` (launchMain needs `ipcMain`; a renderer session is owned by main) and `getStatus`/`relaunch` on `@bugsee/webview` (the carrier holds only a facade). Guard: `dev-packages/packaging-guard/src/adapter-export-parity.test.ts` (add a new umbrella API to its `PUBLIC_RUNTIME_API`; exemptions carry a reason).
- **`launch(appToken, options)`** — the browser composition root (fetch/DOM analog of node's): fetch transport (internal-tagged), api/uploader/upload pipeline, browser env, in-memory store (or IndexedDB-backed when `persist:true`), gated capture providers (console→log; network umbrella, NO `node:http`; system traces; system events; user-interaction input) + detection providers, `client.launch()`. **No `process.exit` path** (the browser flushes via the pipeline/`pagehide`; `stop()` only clears the carrier). `persist:true` builds an IndexedDB durable bundle queue (crash recovery across reload — `recover()` deferred to the store's `whenReady`) + the durable IndexedDB chunk capture store (`createIdbChunkCaptureStore` in its own `bugsee-capture` db). `maxDataSize` defaults to 10 MB. **Session replay is ON by default** (2026-08-27) — parity with the iOS/Android SDKs, which record by default; `replay: false` is the opt-out and is the ONLY value on which `import('@bugsee/replay')` is never evaluated, so an errors-only integration still pays nothing for rrweb (the lazy chunk + the opt-out carry the bundle-size budget, not the default). Masking stays fail-closed on the default path (mask all text/inputs, block all media); `replay.canvas` stays opt-in. Returns the started `BugseeClient`.
- **`launch(appToken, options)`** — the browser composition root (fetch/DOM analog of node's): fetch transport (internal-tagged), api/uploader/upload pipeline, browser env, in-memory store (or IndexedDB-backed when `persist:true`), gated capture providers (console→log; network umbrella, NO `node:http`; system traces; system events; user-interaction input) + detection providers, `client.launch()`. **No `process.exit` path** (the browser flushes via the pipeline/`pagehide`; `stop()` only clears the carrier). `persist:true` builds an IndexedDB durable bundle queue (crash recovery across reload — `recover()` deferred to the store's `whenReady`) + the durable IndexedDB chunk capture store (`createIdbChunkCaptureStore` in its own `bugsee-capture` db). `maxDataSize` defaults to 20 MB (30 MB with the IndexedDB capture store; 2026-11-01). **Session replay is ON by default** (2026-08-27) — parity with the iOS/Android SDKs, which record by default; `replay: false` is the opt-out and is the ONLY value on which `import('@bugsee/replay')` is never evaluated, so an errors-only integration still pays nothing for rrweb (the lazy chunk + the opt-out carry the bundle-size budget, not the default). Masking stays fail-closed on the default path (mask all text/inputs, block all media); `replay.canvas` stays opt-in. Returns the started `BugseeClient`.

### Integration shims — `@bugsee/integration-shims` (tier-3 leaf, slice #13)
No-op stand-ins for DOM-only integrations on DOM-less runtimes (design §372). `createNoopCaptureProvider`/`createNoopInterceptor` (extend `CaptureProviderBase`/`InterceptorBase`) + named shims `createViewHierarchyProviderShim`/`createBreadcrumbsProviderShim`/`createXhrInterceptorShim`. Each is a structurally-valid provider/interceptor that captures nothing and warns ONCE (`logger.warnOnce`, keyed `shim:<name>`, message `<name> is a no-op on <runtime>; ignored`) on ACTIVATION (provider start / interceptor activate) — construction is side-effect-free. Logger (`Pick<Logger,'warnOnce'>`) + runtime label are injected by the platform (runtime-agnostic). **`replay` is intentionally NOT a shim** (design §372: option-driven, ignored-with-warn at option resolution). Per-platform named re-exports land with the platform packages.
Expand Down Expand Up @@ -1444,3 +1444,15 @@ One name, one writer per session (spec `platforms/javascript.md` "Host-owned tra
Verified against current majors, all green with no adapter code changes: `@bugsee/koa` on Koa 3.2 (+ `@types/koa` 3), `@bugsee/nestjs` on Nest 12 (peer range widened to `^9 || ^10 || ^11 || ^12`; unit + real-Nest suites run on 12), `@bugsee/sveltekit` + `sveltekit-e2e` on SvelteKit 3 (fixture migrated: Kit 3 errors on `svelte.config.js`, options are passed to `sveltekit({ adapter })` in `vite.config.ts`; adapter-node 6). Dev deps now track the latest majors, so Nest 9-11, Koa 2 and Kit 2 are no longer exercised in CI. Not done (needs owner decision / separate work): Astro 7 (passes locally on Node 24, but its engine floor is Node >=22.23.4 and CI `node-version: 22` resolved to 22.23.3, so the astro-e2e build was refused; bump CI Node first), Preact 11 smoke, React Router 8 for `@bugsee/remix`, Kit 3 `handleError` status-influence semantics, Rollup 5 prerelease.

**Follow-up (issue #11).** Astro 7 now dev-tested (`astro ^7`, `@astrojs/node ^11`; astro-e2e green); CI `node-version` is `>=22.23.4 <23` in both jobs (Astro 7's engine floor). Preact 11 smoke added, wired into the react `test` and `test:coverage` scripts (`packages/react/vitest.preact.config.ts` aliases `react` to `preact/compat`; `BugseeErrorBoundary` + HOC catch, report and render the fallback). React Router 8: `HandleErrorFunction` is unchanged, but RR's client `onError` passes `{ location, params, pattern, errorInfo }` (React's `ErrorInfo` is nested), so `bugseeOnError` now reads `info.errorInfo.componentStack` (bare `ErrorInfo` still accepted); before, the component stack was silently dropped. Kit 3 `handleError`: tested that an app handler's `{ message, status }` return is forwarded verbatim; reporting gates on the input status. Kit 3 no longer passes a top-level `status` (input is `{ kind, error, issues? }`, status inside `error`), so `effectiveStatus` reads it there for non-`unknown` kinds (kind first: the top-level `status` is a warning getter in Kit 3 dev); before, in production builds every 404 and `error(4xx)` would have been reported. The same filter now guards the client hook in `@bugsee/svelte`, and RR `bugseeOnError` skips thrown `Response`/`data()` control flow. Rollup 5.0.0-0 (prerelease, no notes) left out; keep watching.

### BGSVIEWER-12: replay anchored to a base, visual-first shedding (2026-10-31)
**Defect:** a web error report's replay stayed on one half-loaded page while the URL trace showed several. Root cause: the capture store evicted replay purely by time (window 60 s, rrweb checkout cadence also 60 s), so the surviving window started with incremental events and no Meta/FullSnapshot; the viewer applied them to the wrong DOM. Reproduced on real rrweb 2.2.0 data. A second hole: the shared byte cap evicted in-window logs/events under heavy replay volume.
**Fix:**
- `anchor` flag, out-of-band on `CaptureDataEntry`/`StoredEntry` (never in the payload). The replay recorder flags rrweb's FullSnapshot (type 2) once the guard accepted it, so a dropped snapshot never leaves a base without a snapshot. It travels over the Electron renderer->main IPC as wire field `a: 1` (strictly `1`; anything else from the untrusted renderer is ignored).
- `ChunkCaptureStore` has two lanes (data / visual = replay+video) in one part-number space over the same backend, so snapshot and crash recovery are unchanged. Time eviction keeps the newest anchored part at or before the window start. The byte cap sheds visual first (orphans, then the oldest base segment, a lone segment whole plus a deferred `onVisualReset`) and evicts closed data only when that can make room (an oversized OPEN replay burst no longer wipes the in-window logs).
- `disk-pressure-backend` counts one tick per store tick (distinct `end`), not one per closed part, so the probe backoff / flap window do not run twice as fast while replay is recorded.
- Browser launch: `onVisualReset` -> recorder `checkout()` with exponential backoff (5 s doubling to 5 min, reset after 10 min quiet) so an oversized snapshot cannot loop; replay checkout cadence = `clamp(window/2, 5 s, 60 s)`; an explicit positive `replay.checkoutEveryNms` wins (undefined/0/NaN/non-numbers do not).
**Known limits (by design, for the export step):** the anchor is per PART, so up to ~1 s of records before a base inside the same part can precede it; after a lone-base drop the open part keeps incrementals until a fresh base arrives. Export must trim records that precede the first base (per record, not per part). The flag is held in the in-memory part index only; a store rebuilt from disk after a crash is not re-time-evicted, so it is not needed there.
**Electron (closed 2026-11-01):** main's store is the one with the byte cap, and its `onVisualReset` now tells every renderer to restart its replay recorder through a new control command `checkout` (`@bugsee/node` `onVisualReset` option -> `launchMain` -> `createElectronMainControl().checkout()` -> `onCheckout` in the renderer -> the browser launch's `onReplayRestart` hook). The pacing (5 s, doubling to 5 min, reset after 10 min of quiet) is the shared `createVisualResetGate` from `@bugsee/core`, also used by the browser launch. **Still open:** renderers of several windows share one visual lane in main without a window id on the record, so time eviction keeps only the newest base across windows (per-window lanes are the follow-up).
**Verified:** mutator loop on the lane logic and the new wiring (all mutants killed, hangs caught by timeout); core 1908, replay 228, browser 1339, browser-utils 230, electron 548 tests; coverage gate holds; typecheck, lint, cycles clean; two convergent review rounds.
**Closed 2026-11-01:** the streaming export (`docs/design/streaming-export.md`, slices S0-S6) shipped: `Bundle.body` is a `BundleBody`; reports stream from the store snapshot through `assembleBundleStream` (per-type incremental encoders, fflate streaming zip) into a sink (memory by default, node temp file under `<instance>/export/`, Blob available); window trim + replay clamp/orphan-trim apply to every live report; durable staging streams file bodies (`BundleStore.putFrame`); transports pipe/Blob a `BundleBody`; an export failure falls back to a memory build so the report still ships. Measured live-heap cost of the export: +36 MB -> +4 MB on a 20 MB window (section 9). Default `maxDataSize` raised: browser 20 MB (memory) / 30 MB (IndexedDB); Node 50 MB, edge/worker 10 MB unchanged. **Recovery paths (closed later the same day):** capture-recovery and native-crash-recovery stream too (`recovered-bundle.ts`). **Viewer unzip verified 2026-11-01:** the viewer's own `jszip.min.js` (`JSZip.loadAsync`, what its import dialog uses) opens the streamed bundles (memory and file sink), same members and sizes as the buffered bundle apart from the window-trimmed replay; the import dialog reads only `request.json`, so member order is immaterial. **IndexedDB read path measured and fixed 2026-11-01:** its snapshot read the whole window before yielding; it now streams part by part (export heap +33 -> +21 MB on a 20 MB window; buffered +77 MB; design doc section 9). **Per-window replay (owner decision 2026-11-01: pixels compose per display, reconstruction captures per window; `docs/design/per-window-replay.md`):** slices L0-L3 done (lane on records, per-lane store state and reset, backends attach it, one replay member per window, Electron per-window tagging and `checkout`). **Still open:** viewer support for several replay windows (it reads only the first `replay`), a concrete `compose` for the display video; closed-window lanes are retired (L6); pixel video per display (L4: assembler splits by lane with `attrs.displayId`, `createDisplayVideoSource`, Electron screen/window adapters; the host injects `compose`); the ~20 MB assembler residual of the browser export is the window's largest rrweb event being reified (explained, design doc section 9).
3 changes: 2 additions & 1 deletion docs/design/electron.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,12 @@ backend symbolicates. App native-module symbols upload at build time via `bugsee

| field | on | meaning |
|---|---|---|
| `c` | all | the command: `pause`, `resume`, `flush`, `stop`, `session`, `blackout-start`, `blackout-end`, `window-key` |
| `c` | all | the command: `pause`, `resume`, `flush`, `stop`, `session`, `blackout-start`, `blackout-end`, `window-key`, `checkout` |
| `sid` | `session` | the owner's session id |
| `bo` | `session` | main's blackout state at handshake time (boolean; a renderer that loads mid-blackout starts dark). Always stated by `launchMain` |
| `ho` | `session` | the trace names **main writes itself**, so the renderer must not write them (one writer per name per session; spec `platforms/javascript.md` "Host-owned traces"): `displays` with a `screen`, `capture` with pixel video, `ram_js_heap_{total,used,free}` while a producer named `v8-heap` (`V8_HEAP_PRODUCER_NAME`) is in main's set (the default node set, or a caller set from `createNodeSystemTraceProducers()`); `[]` when main writes no traces. At most 64 names of at most 64 characters; a malformed array is ignored whole. A renderer **never** writes `displays`, `capture` or `ram_js_heap_*`, whatever `ho` says (`ho` can only add names; with no screen, no pixel video or no heap producer the session has none). The heap is detected by the producer name `v8-heap` (`V8_HEAP_PRODUCER_NAME`): that name is the contract, and a custom producer that borrows it without writing the heap makes main declare a heap nobody writes. Absent: the renderer keeps its built-in default (fail closed). Applied to the very next trace value |
| `kw` | `session`, `window-key` | whether the receiving renderer's window is the session's **key window** (spec "Several windows in one session": only the key window writes the per-window traces `url`, `route`, `view`, `orientation`, `app_state`, `fps`, `storage_estimate`, `main_thread_*`, `user_idle_state`). Boolean. Absent on `session`: main cannot resolve windows, so the renderer is key (fail open) |
| `checkout` | main to renderer | sent to every live renderer when main's store shed every replay base under its byte cap (paced by `createVisualResetGate`); the renderer restarts its replay recorder for a fresh full snapshot. No payload |
| `window-key` | main to renderer | sent when the answer **changes** for that renderer (the flag rides in `kw`; a missing flag reads as not key); the renderer suppresses or releases the per-window names and the provider re-records a held value (`url`) when the window becomes key |

**Key-window rule and its limits** (`main-control.ts`, `launch-main.ts`). The key window is the focused `BrowserWindow`, else the most recently focused, else the first created, i.e. the LOWEST window `id` among the live ones, a visible one preferred (`application-tracker.ts`; Electron's `getAllWindows()` is not in creation order: newest first on 44.9.3, unordered after a close; ids only increase). A renderer is key when its owning window (`BrowserWindow.fromWebContents`, else `webContents.getOwnerBrowserWindow()`) is the key window. Fail-open cases, where the renderer stays key rather than being silenced: its window cannot be resolved, the window is **not in `BrowserWindow.getAllWindows()`** (a view hosted in a `BaseWindow`: Electron resolves the BaseWindow's id but never lists it nor fires `browser-window-created` for it), the window list cannot be read, or the probe throws. When the tracker's key window hosts no registered renderer: the key falls back to the last window that did (while it still does). If the tracker's key IS the window whose renderer just left (a closing window's webContents fires `destroyed` before its `closed`; a crash fires `render-process-gone`), or no window has hosted a renderer yet (startup), the key is HELD there (nobody writes, nobody flashes in) until the tracker publishes its own next choice, or for at most 3 s (a crash with no reload never gets a `closed`), after which the window of the earliest registered renderer takes it. A re-evaluation reads the key once, the window list once and `fromWebContents` once per renderer. **Limit:** a key window that still runs the page but no longer the SDK (the page navigated to a document without Bugsee, or called `client.stop()`) keeps its renderer registered, because Electron keeps the same webContents across navigation and the renderer sends no goodbye; while that window stays key, the other windows' per-window traces stay silenced until the key moves (focus, show, hide, close) or the window's SDK starts again (its hello re-registers). A `bye` on stop/pagehide, or a `did-navigate` re-evaluation, would remove the limit and is not implemented. **Known limit (open spec question):** one launch per *window* is assumed. A `BrowserWindow` with its own contents plus several `WebContentsView`s gives every instrumented contents `kw:true` (the same owner), so their `url` values interleave. A cheap rule exists and is **not implemented** (needs an owner decision): within one window the window's own `webContents` is key, else the most recently focused contents (`webContents.isFocused()` / `focus` events). `BaseWindow` windows are not tracked (no listing, no creation event), so they are always key.
Expand Down
Loading
Loading