Skip to content

Overview: JS public API alignment with the mobile SDKs (T1-T9 + integration) - #46

Merged
krassx merged 79 commits into
review/base-0from
main
Oct 12, 2026
Merged

krassx merged 79 commits into
review/base-0from
main

Conversation

@krassx

@krassx krassx commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Review-only PR. This change is already on main (the repo works directly on main; see CLAUDE.md). The base branch is the commit it forked from, so the diff below is exactly this task. Merging this PR changes nothing. Review findings become follow-up commits on main. Plan: docs/design/mobile-api-alignment.md.

Whole-range diff of the alignment work (b77f6ea..main), including what the per-task PRs do not show: the merge-conflict resolutions (type placement of attachments, import/export merges, the Stopped/Stopping ordering) and the BlackoutStarted/Ended wiring (e3e8253). Per-task PRs carry the detailed notes; this one exists so CI and any automated review see the integrated result.

Plan: docs/design/mobile-api-alignment.md. Open decisions: performance sample rate (JS 1.0 vs mobile 0.01) and Node hang detection default.

🤖 Generated with Claude Code

krassx added 30 commits October 12, 2026 07:57
Wire ReportHandler.after (runs once the report pipeline settles, guarded) and add
setLifecycleEventsListener with the Android LifecycleEvents names. Core emits launch/stop,
report assembly and upload outcome (permanent vs retained-for-retry); the browser report
dialog emits BeforeReportShown/AfterReportShown. Constants re-exported from @bugsee/browser
and every @bugsee/bugsee entry.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…, hide wire internals

- UmbrellaExtensionOptions gains performanceUploadMode ('batched' | 'realtime'); type test proves no cast needed.
- startTransaction / startSpan / getActiveSpan on the launched client (browser, node, bun, deno; no-op
  versions on worker, edge-light and workerd and when performance is off), backed by a new
  PerformanceFacade in @bugsee/performance.
- UploadOptions.trigger and LogExceptionOptions.mechanism move to Internal* types reached via
  asInternalClient(); the public BugseeClient no longer accepts them. Internal callers (report dialog,
  adapters, server instrumentation) updated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Conflicts:
#	packages/browser/src/report-dialog.test.ts
#	packages/browser/src/report-dialog.ts
#	packages/bugsee/src/index.deno.ts
Public ReportAttachmentInput ({name, data, mimeType?, fileName?}), writable via
ReportHandler.before (report.userAttachments / addReportAttachment) and
UploadOptions.attachments. Mobile limits (3 x 3 MB) enforced at assembly; drops go to
onError. Bundle: <random20>.attachment.bgsfile + manifest name/attrs (Android layout).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Core owns a tiny idempotent (not counted) BlackoutState, exposed through the
service container (BlackoutToken) with an internal onChange subscription for the
upcoming BlackoutStarted/BlackoutEnded lifecycle events. Visual providers take
a read-only BlackoutGate: the rrweb recorder is stopped for the window and
restarted (fresh full snapshot) at the end, input presses, ui.* breadcrumbs,
ui.interaction spans and the report-time viewtree are suppressed. Logs, network,
traces and events continue. Real-Chromium e2e proves a DOM change made during a
blackout is absent from the report's replay.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Conflicts:
#	packages/browser/src/index.ts
#	packages/core/src/index.test-d.ts
# Conflicts:
#	packages/browser/src/launch.ts
#	packages/bugsee/src/launch.ts
#	packages/core/src/client.test.ts
#	packages/core/src/index.ts
…er and default-priority options

Adds the mobile-parity launch options captureLogsLevel, captureBreadcrumbs (JS default stays true),
detectHttpErrors (http-error detection provider with its own rate budget), captureNetworkDefaultSanitizer,
reportDefaultCrashPriority and reportDefaultErrorPriority, plus getDefaultNetworkSanitizer() so a custom
network filter can keep the default redaction.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Conflicts:
#	packages/bugsee/src/index.bun.ts
#	packages/bugsee/src/index.ts
#	packages/core/src/client.test.ts
#	packages/core/src/client.ts
Adds the mobile launch-state surface: BugseeStatus (Stopped/Launching/Launched/Stopping), client and
process-level getStatus(), relaunch(options?) on the browser, node and umbrella entries, and
getLaunchOptions()/getHostLaunchOptions() keyed by canonical com.bugsee.option.* (registered options only,
never the token or transport).

Fixes found while verifying launch -> stop -> launch:
- browser: the instance Web Lock and the three IndexedDB connections were never released on stop; stop now
  flushes the capture store, waits out the launch-time recovery scan, then releases them
- browser: dead-sibling recovery left its generation cleanup unawaited
- browser/node: a repeat or late stop() cleared the process slot unconditionally and could evict a
  relaunched client; node also ran its teardown twice
- node: a cleanly stopped instance's subtree was never recovered while its pid lived (one stranded subtree
  of capture files per cycle); stop now marks it .stopped so the next launch recovers and removes it

🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Conflicts:
#	packages/browser/src/index.ts
#	packages/bugsee/src/bun.ts
#	packages/bugsee/src/index.bun.ts
#	packages/bugsee/src/index.deno.ts
#	packages/bugsee/src/index.node.ts
#	packages/bugsee/src/index.ts
#	packages/bugsee/src/launch.test.ts
#	packages/bugsee/src/launch.ts
#	packages/bugsee/src/node.ts
#	packages/core/src/client.ts
Adds domain (crash.json exception.domain, Android wire shape) and skipFrames
(marks top N frames hidden so grouping skips them). Findings recorded in
docs/design/mobile-api-alignment.md (T8).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Browser tier only (iOS parity): setUserIdentifier writes to localStorage per app token,
clearUserIdentifier removes it, launch restores it before any report. Opt out with
persistUserIdentifier: false. Storage failures degrade to in-memory via onError.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds client.notify(title, body?, severity?, fields?, urgent?), the mobile Bugsee.notify relay to the app's
messaging integrations. The collector route POST /v2/notifications already accepts browser calls (public
CORS, app token in the body, no session, no app-type check), so no backend change is needed.

- core: createNotifyRelay (in-memory queue, 50 / 256 KB batches, notifyFlushDelay window, urgent skip-ahead,
  2 s to 60 s backoff, per-item failed retry), BugseeApi.notify, notifySender client seam; flush()/stop() deliver
- protocol/core: notifyFlushDelay option (com.bugsee.option.config.notify-flush-delay, default 0)
- browser, node: wire the sender over the existing transport
- docs: T7 findings in mobile-api-alignment.md. Not persisted, unlike mobile.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Conflicts:
#	docs/design/mobile-api-alignment.md
#	packages/protocol/src/options.test.ts
…and null clears

persistUserIdentifier now defaults to false: the identifier is often an email and a
browser may be shared. While off nothing is written, a value left by an earlier
opted-in launch is removed at launch, and clearUserIdentifier() always removes it.
setUserIdentifier(null | undefined | non-string) is a clear and never stores "null".

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…rt parity - review round 1 (G5)

O1 undefined is not supplied; booleans validated; sanitizer fails closed.
O2/O3/O4 detectHttpErrors: 5xx only, per-session de-dup, High severity, HttpError::Server,
attributes, aborts skipped, ids/emails templated. O5 export parity + test. O6 options declared
and provider wired in worker/webview/edge. O7 normalized values written back. O8 documented.
O9 integer log levels. O10 comment. O11 frozen sanitizer. O12 runtime strip of trigger/mechanism.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…- review round 1 (G4)

P1 limits applied at submit; marker stores attachments as base64 and recovery decodes them.
P2 attachment type validation, per-attachment guard. P3 notify user {email}. P4 relay stop/resume
and kill. P5 skipFrames clamp. P6 queue byte/age caps. P7 urgent Set. P8 recovery onError.
P9 empty data, bidi/surrogate filename, domain cap, retryAfter, 403 KillSdk, addReportAttachment exports.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…round 1 (G3)

V1 Electron forwards main's blackout to every renderer (control commands + state on the session reply,
renderers launch dark until the handshake) and gates the pixel video (stop, clear, no snapshot for the span).
V6 performance interactions are dropped by event-time overlap with a blackout span. V7 wheel deltas are gated
when collected and cleared at both edges. V8 recorder/client docs: end the blackout after the sensitive view is
gone. V9 rrweb custom markers bracket the blackout. V10 WebView input/ui/wheel follow the blackout gate.
V11 browser option startInBlackout (com.bugsee.option.capture.start-in-blackout). V12 docs/design/blackout.md,
PROGRESS entry, stale review notes resolved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…d platform parity - review round 1 (G2)

L5/L6 node: .stopped marker only for the latest stop, never after a client.launch(), and only once the
launch-time recovery settled (stop waits for it, bounded by the caller's deadline); a relaunch removes a stale marker.
L7/L14 browser: release the Web Lock + IndexedDB only when drained, recovery settled and no newer stop/launch; a
killed client is released too.
L9: webworker/vercel-edge clear the carrier slot only if they still own it, record status + last launch (relaunch
works); webworker releases its coexistence handles on stop; launchOptionsView passed in webworker, vercel-edge and
webview; umbrella edge-light/worker/workerd entries export getStatus, relaunch, BugseeStatus.
L18: the carrier retains only the integration seams (carrier, clock, scheduler) for relaunch, never the options.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Conflicts:
#	packages/bugsee/src/index.edge-light.ts
#	packages/bugsee/src/index.worker.ts
#	packages/bugsee/src/lifecycle-exports.test.ts
….after (G1)

L1 process-level lifecycle listener (carrier slot + lifecycleEventsListener launch option);
L2 state-before-events with a re-entrancy queue; L3 re-entrant blackout delivery;
L4 stop generations; L8 ReportHandler.after runs before assembly; L10 relaunch resets
options but keeps integration seams; L11 effective launch options; L12 kill-state
Stopping/Stopped; L13 bounded relaunch stop + in-flight launch wait; L16 blackout
refused when not launched; L17 AfterReportShown after removal and sent only if ok;
V2 blackout carried across relaunch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
# Conflicts:
#	packages/browser/src/launch-lifecycle.test.ts
#	packages/core/src/client.ts
#	packages/core/src/relaunch.test.ts
#	packages/node/src/launch-lifecycle.test.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 31208e3 (cfa4677 + the KillSdk uploader test fix). Prior findings were checked against the new guard and tests, not the commit message.

Verified fixed this round

  • launch('x', { carrier: {} }) (same control global) now hits launchedClients.get(global) and returns the restricted facade. The new test pins no second hello and no internal-client brand.
  • { carrier: window.__BUGSEE__ } with the default global is the same hit.
  • APM uploader takes isActive: () => client.isLaunched(). After KillSdk (launched = false) the next flush drains, drops the batch, and stop()s the interval and the realtime store.add wrap. Tests pin both the uploader and wirePerformance.

Overall risk: Medium

Merge recommendation: Request changes for the remaining first-launch door. Review-only PR (already on main); merging is a no-op.

Remaining issue

  1. P1 — { carrier: {}, global: {} } still returns the live client. The new lookup is launchedClients.get(carrierKey(carrier)) ?? launchedClients.get(global). Both keys are page-suppliable options. The comment says the page cannot change the control global; options.global is a public injectable seam on the same launch the IIFE publishes. A fresh pair misses both maps, takes the first-launch path, and returns { ...client, stop } (live filters / stop / INTERNAL brand) plus a new unlatched __bugsee_bridge on that object. The new test never passes both a new carrier and a new global together.

Positive observations

Keying the WeakMap by the control global closes the one-option { carrier: {} } hole that was actually easy to hit. KillSdk no longer POSTs buffered APM: drop-then-stop is the right shape (do not reuse composed stop(), which flushes first).

Did not run the unit/CI suite in this pass.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread packages/webview/src/launch.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review of 31208e3 (cfa4677 + the fire() test fix). Prior P1 { carrier: {} } on the same control global, and P2 KillSdk APM POSTs, were checked against the new code and tests — not the commit messages. One residual of the same P1 class remains.

Findings

  1. P1 packages/webview/src/launch.ts:369,384 — The new fallback keys launchedClients by options.global. That is still page-suppliable (BugseeWebViewLaunchOptions.global; IIFE re-exports this launch unchanged). BugseeWebView.launch('x', { global: {}, carrier: {} }) misses both WeakMap keys and still returns a live client: setNetworkEventFilter (new fetch interceptor wraps globalThis.fetch), stop(), INTERNAL brand. The new test only pins a fresh carrier on the same fake.global.

P2 KillSdk uploader (prior Z64r) is fixed: isActive: () => client.isLaunched() drops the drained batch and stops the interval / realtime wrap. Umbrella stop() still flushes while launched.

Overall risk: High

Merge recommendation: request changes. Do not treat the alignment as done until a page cannot mint a second live client by passing a fresh global.

Most important to fix: ignore options.carrier / options.global once any session in this module is live (or strip both seams in the IIFE wrapper). Pin { global: {}, carrier: {} } after a default-global launch.

Verified fixed in this range: { carrier: {} } / { carrier: window.__BUGSEE__ } with the native global → restricted facade, one hello; KillSdk → no further /v2/performance/transactions POSTs.

I did not run the test suite in this environment (node_modules is empty).

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread packages/webview/src/launch.ts
…, so a fresh global/carrier cannot mint a second live client - PR #46 review

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…nchWithSeams, exported from the package entry

🤖 Generated with [Claude Code](https://claude.com/claude-code)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 968f61d (51d2c1e + conformance launchWithSeams). The remaining P1 was checked against the page-reachable entry and the IIFE export list, not the commit message.

Verified fixed this round

  • Public launch() (what the IIFE publishes as BugseeWebView.launch) strips global, carrier, window, document, clock, scheduler, captureStore, and systemMetricsSampler before calling launchWithSeams. { carrier: {}, global: {} } cannot look like a first launch; those keys never reach the WeakMap. IIFE still exports only launch + VERSION — not launchWithSeams. Tests import launchWithSeams directly; the new publicLaunch test asserts a seam global receives no hello.

Prior findings remain fixed (clear-client, planted lifecycle listener, .dmp marker JSON, decode [null], urgent-at-cap ties, sanitizer numberless tokens, { carrier: {} } on the same global, KillSdk APM drop-then-stop).

Overall risk: Low

Merge recommendation: Approve. This is a review-only PR (already on main); merging it is a no-op. No remaining P0–P3 from this assessment on the untrusted-page surface.

launchWithSeams is exported from the @bugsee/webview package entry for the conformance harness. That is not the IIFE BugseeWebView global; page script in an injected WebView cannot call it through that global.

Positive observations

Stripping seams at the page-reachable wrapper is the right layer (vs adding more WeakMap keys the page can also supply). Native bootstrap still passes transport / controlNonce / captureNonce through, which it must.

Did not run the unit/CI suite in this pass.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 968f61d (51d2c1e + the launchWithSeams harness export). Prior findings were checked against the new page-reachable wrapper and tests, not the commit message. Surrounding callers (IIFE BugseeWebView.launch, launchWithSeams WeakMap keys, restrictedClientFor) were read — not the diff alone.

Verified fixed this round

  • Own-property BugseeWebView.launch('x', { global: {}, carrier: {} }) is stripped in launch() and forwarded to launchWithSeams without those keys, so the lookup hits the host globalThis session and returns the restricted facade. Independently probed: that pair is now restricted.
  • IIFE still exports only launch + VERSION (not launchWithSeams). Native bootstrap BugseeWebView.launch(token, {transport, controlNonce, captureNonce}) keeps those non-seam options.
  • Prior fixes still hold on this tree: clear-client → restricted; seal discards planted lifecycle; marker .dmp dataBase64; dropOldest prefers non-urgent; sanitizer TOKENISH / 16–23 one-digit-run; KillSdk APM isActive: () => client.isLaunched(); pointer #clearGestures; urgent retryTimer.

Overall risk: Medium

Merge recommendation: Request changes. Review-only PR (already on main); merging is a no-op. Do not treat the alignment as done until a page cannot mint a second live client by re-introducing global/carrier on Object.prototype.

Most important issue to fix

  1. P1 — launch() rest object still inherits Object.prototype.global / .carrier. The strip reads those keys off the incoming bag and drops them, then passes a fresh {...} whose [[Get]] of global/carrier sees a polluted prototype. launchWithSeams then keys launchedClients on the planted objects and takes the first-launch path.

Positive observations

Splitting page-reachable launch from launchWithSeams is the right layer (IIFE cannot honour test seams). The own-property pair that was actually easy to hit is closed. KillSdk no longer POSTs buffered APM. Attachment / sanitizer / notify / blackout P1s from earlier rounds stay closed.

I probed the strip+lookup control flow with Node (own-property pair → restricted; Object.prototype.global/carrier → LIVE). I did not run the unit/CI suite (node_modules is empty in this environment).

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread packages/webview/src/launch.ts Outdated
…ted Object.prototype cannot re-introduce them - PR #46 review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 9366ba5 (Object.prototype seam shadow). The remaining P1 was checked against launch() → launchWithSeams lookup, not the commit message.

Verified fixed this round

  • Page-reachable launch() copies only own enumerable non-seam keys, then sets each stripped seam (global, carrier, window, document, clock, scheduler, captureStore, systemMetricsSampler) as an own undefined on the bag it forwards. That shadows Object.prototype.global / .carrier. options.global ?? globalThis and carrierKey(undefined) therefore hit the host session. After a default-global launch, Object.prototype.global = {}; Object.prototype.carrier = {}; BugseeWebView.launch('x') returns the restricted facade (no INTERNAL brand, no second hello on the planted global). Vitest + a throwaway probe confirmed this; the in-tree test pins no hello on a proto-planted global.

Prior findings remain fixed (own-property { global, carrier } strip; IIFE exports only launch + VERSION; { carrier: {} } on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle; marker .dmp dataBase64; dropOldest; sanitizer TOKENISH / 16–23 one-digit-run).

Overall risk: Low

Merge recommendation: Approve. Review-only PR (already on main); merging is a no-op. No remaining P0–P3 on the untrusted-page surface.

Positive observations

Own undefined on the forwarded bag is the right layer: launchWithSeams can keep ordinary [[Get]] for tests, and a polluted prototype cannot re-supply identity keys. Native bootstrap options (transport, controlNonce, captureNonce) are still own-copied.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 9366ba5 (Object.prototype seam shadow). The writable-data P1 is closed; the shadowing method still loses to an accessor (or non-writable) prototype property.

Verified fixed this round

  • Writable Object.prototype.global / .carrier data properties are shadowed. Page-reachable launch() copies only own enumerable non-seam keys, then assigns each stripped seam as an own undefined. options.global ?? globalThis and carrierKey(undefined) therefore hit the host session. The in-tree test (a seam planted on Object.prototype cannot re-enter through the page-reachable launch()) plants data properties and expects no hello on the fake global — I ran it; it passes.

Prior findings remain fixed (own-property { global, carrier } strip; IIFE exports only launch + VERSION; { carrier: {} } on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle; marker .dmp dataBase64; dropOldest; sanitizer TOKENISH / 16–23 one-digit-run).

Remaining issue

P1 — safe[key] = undefined does not create an own property when Object.prototype[key] is an accessor (or a non-writable data property). launchWithSeams still [[Get]]s options.global / options.carrier, so a page getter still re-keys the WeakMap and mints a second LIVE client. Getter-only / readonly descriptors throw TypeError from launch() itself.

Overall risk: Medium

Merge recommendation: Request changes. Review-only PR (already on main); merging is a no-op. Do not treat the untrusted-page surface as closed until the forwarded bag does not consult Object.prototype.

Most important to fix

  1. Build the bag launch() forwards with Object.create(null) (or Object.defineProperty own descriptors), so assignment/get never hits a prototype accessor.
  2. Pin getter+setter and getter-only Object.prototype.global / .carrier probes next to the existing data-property test.

Positive observations

Own undefined was the right layer for writable data properties, and launchWithSeams can keep ordinary [[Get]] for tests. Native bootstrap options (transport, controlNonce, captureNonce) are still own-copied through Object.keys. The in-tree data-property test is the right shape — it just does not cover the accessor bypass the assignment API leaves open.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread packages/webview/src/launch.ts Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 9366ba5 (51d2c1e + own-undefined seam shadow). Surrounding callers, IIFE export, launchWithSeams lookup, APM isActive, and the still-open proto thread were re-checked — not the latest commit in isolation.

Prior P1 (second LIVE client via Object.prototype.global / .carrier) — fixed

launch() now copies only own enumerable keys and writes own undefined for PAGE_STRIPPED_SEAMS. launchWithSeams then sees options.global ?? globalThis === globalThis. Probed:

  • first IIFE-style launch('tok', { captureNetwork: false }) is LIVE on globalThis
  • after Object.prototype.global = {}; Object.prototype.carrier = {}, the next launch() is restricted (no Symbol.for('com.bugsee.internal-client'), setNetworkEventFilter refused)
  • in-tree test a seam planted on Object.prototype cannot re-enter through the page-reachable launch() and the related strip / RELAUNCH tests: 4 passed
  • APM KillSdk / inactive-client tests: 2 passed

The IIFE still exports this launch (iife.ts:9). The established page-after-native path no longer mints a second session.

Remaining finding

P2 — safe is still a normal object. Every option launchWithSeams reads via [[Get]] that is not in PAGE_STRIPPED_SEAMS is still inherited. Reproduced on this SHA: planted Object.prototype.controlNonce makes the first page launch() take the native-secret path (honest canary publishes a JS hello.tok; polluted path publishes none). Planted Object.prototype.lifecycleEventsListener hears Launching / Launched. See inline.

No new P0/P1. Earlier P1s (clear-client, { carrier }, { global, carrier }, allow-list, sanitizer numberless/16–23, marker .dmp, KillSdk APM interval) stay closed.

Overall

  1. Overall risk: Medium
  2. Merge recommendation: Request changes — close the inherited-option hole on the same page entry that was just hardened. The second-LIVE-client class is done.
  3. Most important to fix: Build safe as Object.create(null) (or own-undefined every key launchWithSeams reads: controlNonce, captureNonce, lifecycleEventsListener, transport, reportTrigger, onError, sdkVersion). Pin proto controlNonce + lifecycleEventsListener on a first page launch(), and keep the second-launch restricted assertion.
  4. Positives: The seam strip is the right layer (IIFE === launch); own-undefined actually closes the WeakMap-miss path that 968f61d left open. KillSdk/stop now drop APM batches via isActive: () => client.isLaunched() and stop the interval / realtime wrap. Urgent-at-cap trim prefers non-urgent victims; marker encode covers SDK .dmp bytes.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread packages/webview/src/launch.ts Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 9366ba5 (51d2c1e + own-undefined seam shadow). Callers, IIFE export, launchWithSeams lookup, APM isActive, and the proto-strip were re-checked — not the latest commit in isolation.

Prior P1, writable-data proto — fixed

launch() copies own keys and writes own undefined for PAGE_STRIPPED_SEAMS. A writable Object.prototype.global / .carrier is shadowed. Probed: after a default-global first launch, the next launch() is restricted (no Symbol.for('com.bugsee.internal-client'), setNetworkEventFilter refused). In-tree proto / strip / RELAUNCH tests: 4 passed. APM inactive-client / KillSdk tests: 2 passed.

Remaining P1 — accessor proto still re-enters

safe[key] = undefined does not own-define when Object.prototype[key] is an accessor (setter swallows; getter-only throws in strict). launchWithSeams then [[Get]]s the planted global / carrier → WeakMap miss → second LIVE client. Confirmed with a Node probe of the assignment semantics. See inline.

Also P2 (same root)

safe is not null-prototype, so first page launch() still inherits controlNonce (becomes D-A10 nativeSecret; honest canary publishes hello.tok, polluted path does not) and lifecycleEventsListener (hears Launching / Launched). Repeat launch() after a real first launch stays restricted.

Earlier P1s (clear-client, { carrier }, { global, carrier }, allow-list, sanitizer numberless/16–23, marker .dmp, KillSdk APM interval) stay closed.

Overall

  1. Overall risk: Medium
  2. Merge recommendation: Request changes — Object.create(null) (or defineProperty own data) for the forwarded bag. Writable-data proto is done; accessors reopen the second-LIVE-client path.
  3. Most important to fix: Null-prototype safe. Pin getter+setter and getter-only proto plants (no second LIVE client / no throw). Optionally pin proto controlNonce + lifecycleEventsListener on a first page launch().
  4. Positives: The strip lives on the IIFE launch (right layer). Own-undefined does close the writable-data WeakMap-miss. KillSdk/stop drop APM batches via isActive: () => client.isLaunched(). Urgent-at-cap trim prefers non-urgent victims; marker encode covers SDK .dmp bytes.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread packages/webview/src/launch.ts Outdated
…ype bag, the lifecycle listener option is read as an own property - PR #46 review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at ba608c4 (null-prototype options bag + own-property lifecycle listener). The remaining accessor / inherited-option findings were checked against launch() → launchWithSeams and createClient, not the commit message.

Verified fixed this round

  • Page-reachable launch() now forwards Object.create(null). Missing keys are undefined without consulting Object.prototype, so getter+setter and getter-only plants of global / carrier cannot re-key the WeakMap. After a default-global launch, a getter+setter plant returns the restricted facade (no INTERNAL brand, no second hello). Getter-only no longer throws out of launch(). Vitest + a throwaway probe confirmed this; the in-tree test plants both accessor shapes plus lifecycleEventsListener.
  • Non-seam options (controlNonce, lifecycleEventsListener, …) are not inherited into that bag either. createClient additionally requires Object.hasOwn(options, 'lifecycleEventsListener') before installing the listener.

Prior findings remain fixed (writable-data proto shadow; own-property { global, carrier } strip; IIFE exports only launch + VERSION; { carrier: {} } on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle slot; marker .dmp dataBase64; dropOldest; sanitizer TOKENISH / 16–23 one-digit-run).

Overall risk: Low

Merge recommendation: Approve. Review-only PR (already on main); merging is a no-op. No remaining P0–P3 on the untrusted-page surface.

Positive observations

Object.create(null) is the right layer: one bag, no [[Set]] through inherited accessors, no list of keys that can drift from what launchWithSeams reads. Defense-in-depth in createClient covers launchWithSeams callers that still pass a normal object.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at ba608c4 (null-prototype launch bag + own-property lifecycle listener). The accessor P1 and inherited-option P2 were checked against launch() → launchWithSeams lookup and createClient, not the commit message.

Verified fixed this round

  • Page-reachable launch() forwards Object.create(null) and copies only own enumerable non-seam keys. Missing global / carrier / controlNonce / lifecycleEventsListener are therefore undefined even when Object.prototype has a data property, a getter+setter, or a getter-only accessor. options.global ?? globalThis and carrierKey(undefined) hit the host session; there is no [[Set]] through inherited accessors, so getter-only no longer throws.
  • createClient installs lifecycleEventsListener only via Object.hasOwn, so a planted prototype getter cannot subscribe to Launching/Launched through an options bag that omits it.
  • In-tree tests: writable-data proto plant, accessor (getter+setter global, getter-only carrier) + inherited lifecycleEventsListener, and the core Object.prototype listener test — 4 passed. Node probe of the forwarded bag: Object.getPrototypeOf(safe) === null, safe.global / safe.controlNonce stay undefined under accessor plants.

Prior findings remain fixed (own-property { global, carrier } strip; IIFE exports only launch + VERSION; { carrier: {} } on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle slot; marker .dmp dataBase64; dropOldest; sanitizer TOKENISH / 16–23 one-digit-run).

Overall risk: Low

Merge recommendation: Approve. Review-only PR (already on main); merging is a no-op. No remaining P0–P3 on the untrusted-page surface.

Positive observations

Null-prototype is the right layer: launchWithSeams can keep ordinary [[Get]] for tests, and nothing on Object.prototype can re-supply identity keys or native-only options. Dropping the own-undefined assignment loop is correct once the bag has no prototype — it was the loop that threw on getter-only descriptors. Object.hasOwn in createClient covers non-WebView callers that still pass a normal {}.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at ba608c4 (Object.create(null) page bag + Object.hasOwn for the lifecycle listener). Prior findings were checked against launch / launchWithSeams / createClient / the IIFE surface / sealCarrierLifecycleSlot, not the diff alone.

Prior findings

ID Status
P1 accessor / getter-only proto re-enters global/carrier (own-undefined assign) Fixed. Page launch() now copies own keys into a null-prototype bag and no longer assigns safe[key] = undefined. Getter+setter and getter-only plants cannot reintroduce the seams or throw out of launch(). In-tree test passed.
P2 first page launch() inherits controlNonce / lifecycleEventsListener Fixed. Those keys are not own on an innocent options bag, so they never enter safe. createClient also refuses a proto listener (Object.hasOwn before the read). Extra probe: proto controlNonce (data property and accessor) still publishes a JS-minted 32-hex hello tok; the planted nonce never appears on the wire. Own controlNonce via launchWithSeams still suppresses tok.

BugseeWebView IIFE still exports only launch + VERSION. Repeat page launch() stays restricted. Native { transport, controlNonce, captureNonce } object literals keep their own keys.

Findings

No remaining P0–P3 with a credible failure scenario.


1. Overall risk: Low

2. Merge recommendation: Approve

3. Most important issues to fix

None remaining from this assessment.

4. Positive observations

  • Null-prototype forwarding is the right primitive here: it covers data properties, accessors, getter-only descriptors, and non-seam secrets (controlNonce) without the TypeError the own-undefined loop hit.
  • Object.hasOwn is evaluated before the listener is read, so a throwing proto getter cannot fire on createClient({ appToken, … }).
  • sealCarrierLifecycleSlot(undefined) still seals globalThis via the default argument, matching carrier ?? globalThis on the page path.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 014d37f (specs#49 captureLogs pin). Delta vs ba608c4 is a Node launch test + a kept-difference note in docs/design/mobile-api-alignment.md. Prior untrusted-page findings were not touched.

This hop

  • client.log() writes through captureAggregator and is gated only by kill-state, captureLogsLevel, and setLogEventFilter — not captureLogs. The option still only skips the console interceptor. The new tests pin both sides (explicit line present / console line absent; no log* file when nothing was logged explicitly). Vitest: 2 passed.
  • The design note matches that code. Absent-output bundle entries (not_enabled / no_data) remain out of scope, as stated.

Prior findings remain fixed (null-prototype page launch() bag; Object.hasOwn lifecycle listener; IIFE exports only launch + VERSION; { carrier: {} } / accessor proto cannot mint a second LIVE client; KillSdk APM drop-then-stop; clear-client; marker .dmp dataBase64; dropOldest; sanitizer TOKENISH / 16–23 one-digit-run).

Overall risk: Low

Merge recommendation: Approve. Review-only PR (already on main); merging is a no-op. No remaining P0–P3 from this assessment.

Positive observations

The pin is in the right place: real Node launch() → uploaded zip, same helper as the other T6 option tests, so a future gate on client.log() would fail this file rather than a unit mock of the interceptor.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Deep review — JS/mobile public API alignment (T1–T9)

Re-reviewed at 014d37f (captureLogs pin for specs#49). The new tests were checked against createLogCaptureProvider / client.log() and the existing Node launch path, not the commit message.

This round (014d37f)

  • Test-only + a documented kept difference. captureLogs: false still only gates the console provider (controllingOption = CaptureLogs). client.log() writes to the aggregator with no CaptureLogs check (only killed, captureLogsLevel, and the log filter). That matches specs#49 / PR 52 (Bugsee.log() is never gated).
  • The new end-to-end pins go through real launch() → upload(): with captureLogs: false, console.error does not appear in log* files, client.log('explicit-app-line') does, and a report with no explicit log has no log* file. I ran those two tests; they pass.
  • The note that JS does not emit not_enabled / no_data absent-output entries is accurate for this SDK.

Prior findings remain fixed (null-prototype page bag; Object.hasOwn lifecycle listener; own-property { global, carrier } strip; IIFE exports only launch + VERSION; { carrier: {} } on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle slot; marker .dmp dataBase64; dropOldest; sanitizer TOKENISH / 16–23 one-digit-run). launch.ts still forwards Object.create(null).

Overall risk: Low

Merge recommendation: Approve. Review-only PR (already on main); merging is a no-op. No remaining P0–P3.

Positive observations

Pinning the split on the Node composition root (upload zip, not just the in-memory store) is the right layer for a specs#49 kept difference. client.log() being ungated by captureLogs is the same core method every platform uses.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review @ 014d37f

Re-reviewed HEAD 014d37f06e12589b57b2bcdab9c823b09834482b (prior approve was ba608c4). New commit is test + docs only: captureLogs: false gates automatic console capture; client.log() is still recorded (specs#49 / PR 52).

No new P0–P3. Prior findings remain fixed. I did not re-run the full monorepo suite; I did run the new pin (@bugsee/node mobile-options.launch.test.ts, 14/14 pass).

New commit

The pin matches production:

  • Log provider controllingOption is CaptureLogs. Coordinator skips onStart when the option is off, so the console interceptor never gets a subscriber and does not patch console.
  • client.log() writes straight to the capture aggregator. It is not gated by captureLogs — only killed, captureLogsLevel, and the user log filter (packages/core/src/client.ts). That is the iOS-shaped split documented under Kept differences.
  • Second case is real: with the option off and no explicit log(), the bundle has no log* member. console.error in this harness is captured when the option is on (sibling captureLogsLevel test).

Not a defect: log() / event() / trace() / addBreadcrumb() still accept writes after stop() (only killed is checked). Upload-path manuals (logException, upload, notify) check stopped. That split is documented and tested.

Prior findings (spot-checked at this SHA)

Area Status
WebView page launch() Null-prototype bag; seams stripped; IIFE exports launch + VERSION only
Inherited controlNonce / lifecycleEventsListener Not visible on the null-prototype copy (Object.keys own keys only)
Notify failed-urgent at cap, equal timestamps dropOldest prefers non-urgent; frozen-clock test pins it
Marker .dmp JSON hop encodeRequestForMarker base64-encodes SDK report.attachments as well as user attachments
Input blackout pointer maps #watchBlackout → #clearGestures
APM after stop() / KillSdk Uploader isActive → client.isLaunched(); inactive flush drops and stop()s
relaunchEdge public leak Gone from package indexes
User-identifier persist Failed setItem removes the stale key; persistUserIdentifier: false purges

Residual (not P0–P3): KillSdk does not call umbrella wired.stop() in the same turn; the interval dies on the next flush tick via isActive (bounded by flushIntervalMs).

Verdict

  1. Overall risk: Low
  2. Merge recommendation: Approve
  3. Most important issues to fix: None remaining
  4. Positive: The specs#49 pin is the right kind of test — end-to-end through the Node composition, asserting both the keep (client.log) and the drop (console + empty log stream). Earlier rounds closed the WebView second-client, attachment JSON-hop, notify-cap, blackout, and APM-after-stop holes.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@krassx
krassx merged commit 23a2008 into review/base-0 Oct 12, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant