diff --git a/docs/design/plans/2026-09-16-implementation-plan.md b/docs/design/plans/2026-09-16-implementation-plan.md index 9f0ad58..6014144 100644 --- a/docs/design/plans/2026-09-16-implementation-plan.md +++ b/docs/design/plans/2026-09-16-implementation-plan.md @@ -4037,7 +4037,7 @@ The phase with the most native↔JS round-tripping, hence the most device testin - [x] **9.5** `addBreadcrumb(crumb)` and `setBreadcrumbFilter(cb)`. Breadcrumbs are built through the SDK's exchange factory on both platforms, so the bridge constructs rather than forwards. Device: WOD_LX1 `AMRJCP4718402860` retained `breadcrumb-filter immediate 30dccd7246d8 REDACTED` and `breadcrumb-filter rewrite 30dccd7246d8 REDACTED` at level `info`, and the unsettled crumb was absent; simulator `6FA9B3E8-26C7-4232-AA2C-537D9DF32957` retained `breadcrumb-filter immediate e62610c1b82b REDACTED` and `breadcrumb-filter rewrite e62610c1b82b REDACTED` at level `info`, and the unsettled crumb was absent. The iPhone XS (KRSFT) was already running BareExample (pid 43281), so that run was not taken. - [x] **9.6** `addNetworkEvent` for stacks the SDK does not auto-instrument. WOD_LX1 `AMRJCP4718402860` retained `https://bugsee-e2e.invalid/add/89dc3cdd4491` (`mechanism` `react-native`, `type` `complete`) and omitted the hung url (the filter logged `hung nonce=89dc3cdd4491`). iOS builds a `BugseeNetworkEvent` (`eventWithID:`) because beta3's factory `createNetworkEvent` returns nil, then `addNetworkEvent:requiresFiltering:YES`. Simulator `6FA9B3E8-26C7-4232-AA2C-537D9DF32957` retained `https://bugsee-e2e.invalid/add/0b8a3f41b546` and omitted the hung url (filter logged `hung nonce=0b8a3f41b546`). iPhone XS KRSFT `345BA7FE-2C29-5722-892A-BFCB1FD34D0C` retained `https://bugsee-e2e.invalid/add/5013161d5890` and omitted the hung url (`hung nonce=5013161d5890`). Both events are `mechanism` `react-native`, `type` `complete`. `duration` stayed 90. - [x] **9.7** Dedup across the two console streams, *before* either filter runs — otherwise the user's callback runs twice on one line by two routes. Device test in **both** Debug and Release; RN only routes `console.*` through `RCTLog` under `__DEV__`, and Hermes release builds commonly strip console calls. After the one-shot stdout/stderr echo drop was restored, WOD_LX1 `AMRJCP4718402860` Debug retained one `BUGSEE_E2E dedup-line 9370533d0f4d #1` (source 98, level 3, `calls=1`, `dev=true`) and Release retained one `BUGSEE_E2E dedup-line 5269f19051b7 #1` (`dev=false`, `calls=1`). Simulator `6FA9B3E8-26C7-4232-AA2C-537D9DF32957` Debug retained one `BUGSEE_E2E dedup-line 5d34148a4685 #1` (source 98, level 3, `calls=1`, `dev=true`) and Release retained one `BUGSEE_E2E dedup-line dd8a7164a049 #1` (`dev=false`, `calls=1`). iPhone XS KRSFT was running BareExample, so that run was not taken. -- [ ] Review gate. +- [x] Review gate. Passed at `2b56960`: an unfinished log, network, or breadcrumb filter drops the line, the Android borrow stays 9s, the iOS data-request deadline stays 450ms, `patchXhr` and a JS `fetch` patch stay off, and console dedup still drops the native echo before the user callback. One minor stays out: one Android message can accumulate more than 32 echo credits inside a single append. `setLogFilter` now subscribes before it enables the native filter. --- diff --git a/packages/react-native/src/logs/__tests__/filter.test.ts b/packages/react-native/src/logs/__tests__/filter.test.ts index 80f0944..207cb0c 100644 --- a/packages/react-native/src/logs/__tests__/filter.test.ts +++ b/packages/react-native/src/logs/__tests__/filter.test.ts @@ -159,6 +159,20 @@ describe('setLogFilter', () => { expect(native.logFilterRequestSubscribeCallCount()).toBe(1); }); + it('subscribes once, and before the native filter is installed', () => { + const original = native.onLogFilterRequest.bind(native); + native.onLogFilterRequest = (listener) => { + expect(native.setLogFilterEnabled).not.toHaveBeenCalled(); + return original(listener); + }; + Bugsee.setLogFilter((line) => line); + expect(native.setLogFilterEnabled).toHaveBeenCalledWith(true); + Bugsee.setLogFilter((line) => line); + Bugsee.setLogFilter(); + Bugsee.setLogFilter((line) => line); + expect(native.logFilterRequestSubscribeCallCount()).toBe(1); + }); + it('rejects a non-function and keeps the filter already installed', async () => { Bugsee.setLogFilter((line) => `kept:${line}`); const set = Bugsee.setLogFilter as (callback?: unknown) => void; diff --git a/packages/react-native/src/logs/filter.ts b/packages/react-native/src/logs/filter.ts index 8ce837b..74b8fc3 100644 --- a/packages/react-native/src/logs/filter.ts +++ b/packages/react-native/src/logs/filter.ts @@ -22,9 +22,11 @@ let subscribed = false; * `undefined` or `null` clears it, which tells native to uninstall the filter * so lines are recorded without one. * - * The native request is subscribed to once and stays subscribed: clearing the - * callback stops native from asking, and a later callback must still be able - * to answer. + * The JS listener is subscribed before the native filter is installed, so a + * line filtered on the installing turn still has a listener. On iOS a request + * emitted in that gap stays in the pending table until process death. The + * subscription stays: clearing the callback stops native from asking, and a + * later callback must still be able to answer. */ export function setLogFilter(callback?: LogFilter | null): void { if (callback != null && typeof callback !== 'function') { @@ -32,12 +34,12 @@ export function setLogFilter(callback?: LogFilter | null): void { `Bugsee.setLogFilter requires a function, got ${typeof callback}`, ); } - current = callback ?? undefined; - NativeBugsee.setLogFilterEnabled(current !== undefined); - if (current !== undefined && !subscribed) { - subscribed = true; + if (callback != null && !subscribed) { NativeBugsee.onLogFilterRequest(onLogFilterRequest); + subscribed = true; } + current = callback ?? undefined; + NativeBugsee.setLogFilterEnabled(current !== undefined); } /**