Repository navigation
Overview: JS public API alignment with the mobile SDKs (T1-T9 + integration) - #46
Conversation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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
…d T3 🤖 Generated with [Claude Code](https://claude.com/claude-code)
# 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
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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)
…ent work 🤖 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
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
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 controlglobal) now hitslaunchedClients.get(global)and returns the restricted facade. The new test pins no secondhelloand 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, andstop()s the interval and the realtimestore.addwrap. Tests pin both the uploader andwirePerformance.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
- P1 —
{ carrier: {}, global: {} }still returns the live client. The new lookup islaunchedClients.get(carrierKey(carrier)) ?? launchedClients.get(global). Both keys are page-suppliable options. The comment says the page cannot change the control global;options.globalis a public injectable seam on the samelaunchthe 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_bridgeon 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 composedstop(), which flushes first).Did not run the unit/CI suite in this pass.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review of
31208e3(cfa4677+ thefire()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
- P1
packages/webview/src/launch.ts:369,384— The new fallback keyslaunchedClientsbyoptions.global. That is still page-suppliable (BugseeWebViewLaunchOptions.global; IIFE re-exports thislaunchunchanged).BugseeWebView.launch('x', { global: {}, carrier: {} })misses both WeakMap keys and still returns a live client:setNetworkEventFilter(new fetch interceptor wrapsglobalThis.fetch),stop(), INTERNAL brand. The new test only pins a freshcarrieron the samefake.global.P2 KillSdk uploader (prior Z64r) is fixed:
isActive: () => client.isLaunched()drops the drained batch and stops the interval / realtime wrap. Umbrellastop()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.globalonce 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 nativeglobal→ restricted facade, onehello; KillSdk → no further/v2/performance/transactionsPOSTs.I did not run the test suite in this environment (
node_modulesis empty).Sent by Cursor Automation: Bugsee code review
…, 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)
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at 968f61d (
51d2c1e+ conformancelaunchWithSeams). 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 asBugseeWebView.launch) stripsglobal,carrier,window,document,clock,scheduler,captureStore, andsystemMetricsSamplerbefore callinglaunchWithSeams.{ carrier: {}, global: {} }cannot look like a first launch; those keys never reach the WeakMap. IIFE still exports onlylaunch+VERSION— notlaunchWithSeams. Tests importlaunchWithSeamsdirectly; the newpublicLaunchtest asserts a seamglobalreceives nohello.Prior findings remain fixed (clear-
client, planted lifecycle listener,.dmpmarker 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.
launchWithSeamsis exported from the@bugsee/webviewpackage entry for the conformance harness. That is not the IIFEBugseeWebViewglobal; 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/captureNoncethrough, which it must.Did not run the unit/CI suite in this pass.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at 968f61d (
51d2c1e+ thelaunchWithSeamsharness export). Prior findings were checked against the new page-reachable wrapper and tests, not the commit message. Surrounding callers (IIFEBugseeWebView.launch,launchWithSeamsWeakMap keys,restrictedClientFor) were read — not the diff alone.Verified fixed this round
- Own-property
BugseeWebView.launch('x', { global: {}, carrier: {} })is stripped inlaunch()and forwarded tolaunchWithSeamswithout those keys, so the lookup hits the hostglobalThissession and returns the restricted facade. Independently probed: that pair is nowrestricted.- IIFE still exports only
launch+VERSION(notlaunchWithSeams). Native bootstrapBugseeWebView.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.dmpdataBase64;dropOldestprefers non-urgent; sanitizer TOKENISH / 16–23 one-digit-run; KillSdk APMisActive: () => client.isLaunched(); pointer#clearGestures; urgentretryTimer.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-introducingglobal/carrieronObject.prototype.Most important issue to fix
- P1 —
launch()rest object still inheritsObject.prototype.global/.carrier. The strip reads those keys off the incoming bag and drops them, then passes a fresh{...}whose[[Get]]ofglobal/carriersees a polluted prototype.launchWithSeamsthen keyslaunchedClientson the planted objects and takes the first-launch path.Positive observations
Splitting page-reachable
launchfromlaunchWithSeamsis 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_modulesis empty in this environment).Sent by Cursor Automation: Bugsee code review
…ted Object.prototype cannot re-introduce them - PR #46 review 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at 9366ba5 (
Object.prototypeseam shadow). The remaining P1 was checked againstlaunch()→launchWithSeamslookup, 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 ownundefinedon the bag it forwards. That shadowsObject.prototype.global/.carrier.options.global ?? globalThisandcarrierKey(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 secondhelloon the planted global). Vitest + a throwaway probe confirmed this; the in-tree test pins nohelloon a proto-plantedglobal.Prior findings remain fixed (own-property
{ global, carrier }strip; IIFE exports onlylaunch+VERSION;{ carrier: {} }on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle; marker.dmpdataBase64;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
undefinedon the forwarded bag is the right layer:launchWithSeamscan keep ordinary[[Get]]for tests, and a polluted prototype cannot re-supply identity keys. Native bootstrap options (transport,controlNonce,captureNonce) are still own-copied.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at 9366ba5 (
Object.prototypeseam 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/.carrierdata properties are shadowed. Page-reachablelaunch()copies only own enumerable non-seam keys, then assigns each stripped seam as an ownundefined.options.global ?? globalThisandcarrierKey(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 nohelloon the fake global — I ran it; it passes.Prior findings remain fixed (own-property
{ global, carrier }strip; IIFE exports onlylaunch+VERSION;{ carrier: {} }on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle; marker.dmpdataBase64;dropOldest; sanitizer TOKENISH / 16–23 one-digit-run).Remaining issue
P1 —
safe[key] = undefineddoes not create an own property whenObject.prototype[key]is an accessor (or a non-writable data property).launchWithSeamsstill[[Get]]soptions.global/options.carrier, so a page getter still re-keys the WeakMap and mints a second LIVE client. Getter-only / readonly descriptors throwTypeErrorfromlaunch()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 consultObject.prototype.Most important to fix
- Build the bag
launch()forwards withObject.create(null)(orObject.definePropertyown descriptors), so assignment/get never hits a prototype accessor.- Pin getter+setter and getter-only
Object.prototype.global/.carrierprobes next to the existing data-property test.Positive observations
Own
undefinedwas the right layer for writable data properties, andlaunchWithSeamscan keep ordinary[[Get]]for tests. Native bootstrap options (transport,controlNonce,captureNonce) are still own-copied throughObject.keys. The in-tree data-property test is the right shape — it just does not cover the accessor bypass the assignment API leaves open.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at 9366ba5 (
51d2c1e+ own-undefinedseam shadow). Surrounding callers, IIFE export,launchWithSeamslookup, APMisActive, 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 ownundefinedforPAGE_STRIPPED_SEAMS.launchWithSeamsthen seesoptions.global ?? globalThis === globalThis. Probed:
- first IIFE-style
launch('tok', { captureNetwork: false })is LIVE onglobalThis- after
Object.prototype.global = {}; Object.prototype.carrier = {}, the nextlaunch()is restricted (noSymbol.for('com.bugsee.internal-client'),setNetworkEventFilterrefused)- 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 —
safeis still a normal object. Every optionlaunchWithSeamsreads via[[Get]]that is not inPAGE_STRIPPED_SEAMSis still inherited. Reproduced on this SHA: plantedObject.prototype.controlNoncemakes the first pagelaunch()take the native-secret path (honest canary publishes a JShello.tok; polluted path publishes none). PlantedObject.prototype.lifecycleEventsListenerhearsLaunching/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
- Overall risk: Medium
- 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.
- Most important to fix: Build
safeasObject.create(null)(or own-undefinedevery keylaunchWithSeamsreads:controlNonce,captureNonce,lifecycleEventsListener,transport,reportTrigger,onError,sdkVersion). Pin protocontrolNonce+lifecycleEventsListeneron a first pagelaunch(), and keep the second-launch restricted assertion.- Positives: The seam strip is the right layer (IIFE ===
launch); own-undefinedactually closes the WeakMap-miss path that 968f61d left open. KillSdk/stop now drop APM batches viaisActive: () => client.isLaunched()and stop the interval / realtime wrap. Urgent-at-cap trim prefers non-urgent victims; marker encode covers SDK.dmpbytes.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at 9366ba5 (
51d2c1e+ own-undefinedseam shadow). Callers, IIFE export,launchWithSeamslookup, APMisActive, 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 ownundefinedforPAGE_STRIPPED_SEAMS. A writableObject.prototype.global/.carrieris shadowed. Probed: after a default-global first launch, the nextlaunch()is restricted (noSymbol.for('com.bugsee.internal-client'),setNetworkEventFilterrefused). In-tree proto / strip / RELAUNCH tests: 4 passed. APM inactive-client / KillSdk tests: 2 passed.Remaining P1 — accessor proto still re-enters
safe[key] = undefineddoes not own-define whenObject.prototype[key]is an accessor (setter swallows; getter-only throws in strict).launchWithSeamsthen[[Get]]s the plantedglobal/carrier→ WeakMap miss → second LIVE client. Confirmed with a Node probe of the assignment semantics. See inline.Also P2 (same root)
safeis not null-prototype, so first pagelaunch()still inheritscontrolNonce(becomes D-A10nativeSecret; honest canary publisheshello.tok, polluted path does not) andlifecycleEventsListener(hearsLaunching/Launched). Repeatlaunch()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
- Overall risk: Medium
- Merge recommendation: Request changes —
Object.create(null)(ordefinePropertyown data) for the forwarded bag. Writable-data proto is done; accessors reopen the second-LIVE-client path.- Most important to fix: Null-prototype
safe. Pin getter+setter and getter-only proto plants (no second LIVE client / no throw). Optionally pin protocontrolNonce+lifecycleEventsListeneron a first pagelaunch().- Positives: The strip lives on the IIFE
launch(right layer). Own-undefineddoes close the writable-data WeakMap-miss. KillSdk/stop drop APM batches viaisActive: () => client.isLaunched(). Urgent-at-cap trim prefers non-urgent victims; marker encode covers SDK.dmpbytes.Sent by Cursor Automation: Bugsee code review
…ype bag, the lifecycle listener option is read as an own property - PR #46 review 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
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()→launchWithSeamsandcreateClient, not the commit message.Verified fixed this round
- Page-reachable
launch()now forwardsObject.create(null). Missing keys areundefinedwithout consultingObject.prototype, so getter+setter and getter-only plants ofglobal/carriercannot re-key the WeakMap. After a default-global launch, a getter+setter plant returns the restricted facade (no INTERNAL brand, no secondhello). Getter-only no longer throws out oflaunch(). Vitest + a throwaway probe confirmed this; the in-tree test plants both accessor shapes pluslifecycleEventsListener.- Non-seam options (
controlNonce,lifecycleEventsListener, …) are not inherited into that bag either.createClientadditionally requiresObject.hasOwn(options, 'lifecycleEventsListener')before installing the listener.Prior findings remain fixed (writable-data proto shadow; own-property
{ global, carrier }strip; IIFE exports onlylaunch+VERSION;{ carrier: {} }on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle slot; marker.dmpdataBase64;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 whatlaunchWithSeamsreads. Defense-in-depth increateClientcoverslaunchWithSeamscallers that still pass a normal object.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
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()→launchWithSeamslookup andcreateClient, not the commit message.Verified fixed this round
- Page-reachable
launch()forwardsObject.create(null)and copies only own enumerable non-seam keys. Missingglobal/carrier/controlNonce/lifecycleEventsListenerare thereforeundefinedeven whenObject.prototypehas a data property, a getter+setter, or a getter-only accessor.options.global ?? globalThisandcarrierKey(undefined)hit the host session; there is no[[Set]]through inherited accessors, so getter-only no longer throws.createClientinstallslifecycleEventsListeneronly viaObject.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-onlycarrier) + inheritedlifecycleEventsListener, and the coreObject.prototypelistener test — 4 passed. Node probe of the forwarded bag:Object.getPrototypeOf(safe) === null,safe.global/safe.controlNoncestayundefinedunder accessor plants.Prior findings remain fixed (own-property
{ global, carrier }strip; IIFE exports onlylaunch+VERSION;{ carrier: {} }on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle slot; marker.dmpdataBase64;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:
launchWithSeamscan keep ordinary[[Get]]for tests, and nothing onObject.prototypecan re-supply identity keys or native-only options. Dropping the own-undefinedassignment loop is correct once the bag has no prototype — it was the loop that threw on getter-only descriptors.Object.hasOwnincreateClientcovers non-WebView callers that still pass a normal{}.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at ba608c4 (
Object.create(null)page bag +Object.hasOwnfor the lifecycle listener). Prior findings were checked againstlaunch/launchWithSeams/createClient/ the IIFE surface /sealCarrierLifecycleSlot, not the diff alone.Prior findings
ID Status P1 accessor / getter-only proto re-enters global/carrier(own-undefinedassign)Fixed. Page launch()now copies own keys into a null-prototype bag and no longer assignssafe[key] = undefined. Getter+setter and getter-only plants cannot reintroduce the seams or throw out oflaunch(). In-tree test passed.P2 first page launch()inheritscontrolNonce/lifecycleEventsListenerFixed. Those keys are not own on an innocent options bag, so they never enter safe.createClientalso refuses a proto listener (Object.hasOwnbefore the read). Extra probe: protocontrolNonce(data property and accessor) still publishes a JS-minted 32-hex hellotok; the planted nonce never appears on the wire. OwncontrolNoncevialaunchWithSeamsstill suppressestok.
BugseeWebViewIIFE still exports onlylaunch+VERSION. Repeat pagelaunch()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-undefinedloop hit.Object.hasOwnis evaluated before the listener is read, so a throwing proto getter cannot fire oncreateClient({ appToken, … }).sealCarrierLifecycleSlot(undefined)still sealsglobalThisvia the default argument, matchingcarrier ?? globalThison the page path.Sent by Cursor Automation: Bugsee code review
…nt.log() (specs#49) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at 014d37f (specs#49
captureLogspin). Delta vs ba608c4 is a Node launch test + a kept-difference note indocs/design/mobile-api-alignment.md. Prior untrusted-page findings were not touched.This hop
client.log()writes throughcaptureAggregatorand is gated only by kill-state,captureLogsLevel, andsetLogEventFilter— notcaptureLogs. The option still only skips the console interceptor. The new tests pin both sides (explicit line present / console line absent; nolog*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.hasOwnlifecycle listener; IIFE exports onlylaunch+VERSION;{ carrier: {} }/ accessor proto cannot mint a second LIVE client; KillSdk APM drop-then-stop; clear-client; marker.dmpdataBase64;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 onclient.log()would fail this file rather than a unit mock of the interceptor.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep review — JS/mobile public API alignment (T1–T9)
Re-reviewed at 014d37f (
captureLogspin for specs#49). The new tests were checked againstcreateLogCaptureProvider/client.log()and the existing Node launch path, not the commit message.This round (
014d37f)
- Test-only + a documented kept difference.
captureLogs: falsestill only gates the console provider (controllingOption = CaptureLogs).client.log()writes to the aggregator with noCaptureLogscheck (onlykilled,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(): withcaptureLogs: false,console.errordoes not appear inlog*files,client.log('explicit-app-line')does, and a report with no explicit log has nolog*file. I ran those two tests; they pass.- The note that JS does not emit
not_enabled/no_dataabsent-output entries is accurate for this SDK.Prior findings remain fixed (null-prototype page bag;
Object.hasOwnlifecycle listener; own-property{ global, carrier }strip; IIFE exports onlylaunch+VERSION;{ carrier: {} }on the same global; KillSdk APM drop-then-stop; clear-client; planted lifecycle slot; marker.dmpdataBase64;dropOldest; sanitizer TOKENISH / 16–23 one-digit-run).launch.tsstill forwardsObject.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 bycaptureLogsis the same core method every platform uses.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
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
controllingOptionisCaptureLogs. Coordinator skipsonStartwhen the option is off, so the console interceptor never gets a subscriber and does not patchconsole. client.log()writes straight to the capture aggregator. It is not gated bycaptureLogs— onlykilled,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 nolog*member.console.errorin this harness is captured when the option is on (siblingcaptureLogsLeveltest).
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
- Overall risk: Low
- Merge recommendation: Approve
- Most important issues to fix: None remaining
- 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.
Sent by Cursor Automation: Bugsee code review


Whole-range diff of the alignment work (
b77f6ea..main), including what the per-task PRs do not show: the merge-conflict resolutions (type placement ofattachments, 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