diff --git a/packages/react-native/README.md b/packages/react-native/README.md index f9e9c36e..7250d06e 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -285,6 +285,55 @@ on Bugsee's production API. Set, it reaches every upload and the runtime: Each of these is the plugin's own, so a later prebuild with a new value replaces it, and one without `endpoint` removes it again. +### Auto-launch: what is recorded before JavaScript runs (Android) + +With `autoLaunch` the Android SDK starts at process start, from manifest +meta-data, before any JavaScript exists. Two things follow: + +- **Reports say which wrapper they came from, with context.** The native side + registers only the wrapper type and version at process start; `Bugsee.attach()` + registers the rest (React Native version, JS engine, build configuration), as + `launch()` and `relaunch()` do. Call `attach()` once at startup. +- **Filters are not installed yet.** The JS log, network and breadcrumb filters + are installed natively only when `setLogFilter`, `setNetworkFilter` and + `setBreadcrumbFilter` run, so what the SDK captures before then is recorded + unfiltered. To close that window, opt in per filter: + + ```json + ["@bugsee/react-native", { "autoLaunch": true, "appToken": "", "holdFilters": ["network", "log", "breadcrumb"] }] + ``` + + `true` holds all three. A held filter is installed natively at process start + and drops every event of its kind (log lines, network events, breadcrumbs) + until JS calls its setter, with a function (your filter applies) or with + `null` (no filter: events pass through). Call each setter unconditionally, + early, on every start of the JS runtime. Only list the filters your app + installs: an app that lists one and never calls its setter records none of + that kind, and the wrapper says so in the log (`BugseeRN`, warning) 30 s after + process start. It is off by default for that reason, and an app that never uses + filters is unaffected. It writes `com.bugsee.reactnative.hold-filters` + application meta-data, removed again by a prebuild without the option. + A JS reload is held only if the previous runtime had a filter function + installed (it stays dropped until the new runtime calls the setter again). If + the previous runtime called `setXFilter(null)`, that kind passes through again + from the reload until the new runtime calls the setter. The 30 s warning is + given once per process, at 30 s after process start. The hold is meant + for `autoLaunch`; it was not + verified with a JS `launch()`, where the SDK may replace a filter set before + launch. In a process that never starts React Native (a service or push + process), a held kind drops everything native code records, for the life of + that process. + Two interactions to know about: a filter you declare in the manifest + (`com.bugsee.filter.log-event`, `network-event` or `breadcrumb`) is not + applied for a kind that is held, because the SDK lets a filter set from code + take its place, so do not list a kind you filter from the manifest; and + `setXFilter(null)` then leaves that kind with no filter at all. +- **Masking is not held.** Nothing from `` or `setSecureRectangles` + is masked until those mount, and the wrapper cannot hold the SDK's capture + from JS-side state. If the first frames must not show a sensitive screen, do + not auto-launch: call `Bugsee.launch()` from JavaScript (or keep `autoLaunch` + off), after your first secure view is in place. + The plugin's Gradle edits never remove or change a line of your own code, on a clean or a `--no-clean` prebuild, however often it runs: it adds its own marked lines, replaces only those, and rewrites exactly one of yours, diff --git a/packages/react-native/android/src/main/java/com/bugsee/reactnative/BreadcrumbFilterBridge.java b/packages/react-native/android/src/main/java/com/bugsee/reactnative/BreadcrumbFilterBridge.java index 55e7137c..2df5acfc 100644 --- a/packages/react-native/android/src/main/java/com/bugsee/reactnative/BreadcrumbFilterBridge.java +++ b/packages/react-native/android/src/main/java/com/bugsee/reactnative/BreadcrumbFilterBridge.java @@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.function.LongSupplier; /** @@ -108,7 +109,7 @@ private static final class Pending { } } - private static final BreadcrumbFilterBridge SHARED = new BreadcrumbFilterBridge(new DaemonScheduler()); + private static final BreadcrumbFilterBridge SHARED = new BreadcrumbFilterBridge(new DaemonScheduler(), System::nanoTime, Bugsee::setBreadcrumbFilter); @NonNull static BreadcrumbFilterBridge shared() { @@ -147,8 +148,64 @@ static BreadcrumbFilterBridge shared() { /** Tests also pass a clock, to pass the deadline without the timer having run. */ BreadcrumbFilterBridge(@NonNull final Scheduler scheduler, @NonNull final LongSupplier nanoClock) { + this(scheduler, nanoClock, installed -> { }); + } + + /** As above, and the call that installs (or, with null, removes) the SDK's filter. */ + BreadcrumbFilterBridge( + @NonNull final Scheduler scheduler, + @NonNull final LongSupplier nanoClock, + @NonNull final Consumer> installer + ) { this.scheduler = scheduler; this.nanoClock = nanoClock; + this.installer = installer; + } + + private final Consumer> installer; + + /** True from {@link #holdUntilJs} until JS first calls {@link #setEnabled}. For the watchdog only. */ + private final AtomicBoolean held = new AtomicBoolean(); + + /** Whether the startup hold is still waiting for JS to call {@code setBreadcrumbFilter}. */ + boolean isHeld() { + return held.get(); + } + + /** + * Installs the filter with the SDK. A throw (the SDK not ready that early) + * leaves {@code installed} false, so a later {@code setEnabled(true)} + * installs it instead of believing it already is, and is rethrown for the + * caller to log. + */ + private void install() { + if (installed.compareAndSet(false, true)) { + boolean ok = false; + try { + installer.accept(filter); + ok = true; + } finally { + if (!ok) { + installed.set(false); + } + } + } + } + + /** + * Installs the filter before JS has asked for it, so a manifest + * auto-launch does not record breadcrumbs unfiltered until + * {@code setBreadcrumbFilter} runs (see {@link StartupHold}). With no module + * attached yet the filter drops everything, and so does one whose JS has + * not armed it (the armed gate), so nothing waits on a reply that cannot + * come. {@code setEnabled(false)}, which {@code setBreadcrumbFilter(null)} sends, removes + * it again. A second call, or one after {@code setEnabled(true)}, is a no-op. + */ + void holdUntilJs() { + withSdkLock(() -> { + install(); + held.set(true); + }); } private final EventFilter filter = new EventFilter() { @@ -243,17 +300,16 @@ void setEnabled(final boolean enabled, final Sink caller) { // A superseded module must neither arm nor disable the process-wide filter. return; } + held.set(false); if (enabled) { if (caller != null) { arm(caller); } - if (installed.compareAndSet(false, true)) { - Bugsee.setBreadcrumbFilter(filter); - } + install(); return; } if (installed.compareAndSet(true, false)) { - Bugsee.setBreadcrumbFilter(null); + installer.accept(null); } }); } diff --git a/packages/react-native/android/src/main/java/com/bugsee/reactnative/LogFilterBridge.java b/packages/react-native/android/src/main/java/com/bugsee/reactnative/LogFilterBridge.java index ecdb38a9..cee6a18e 100644 --- a/packages/react-native/android/src/main/java/com/bugsee/reactnative/LogFilterBridge.java +++ b/packages/react-native/android/src/main/java/com/bugsee/reactnative/LogFilterBridge.java @@ -132,6 +132,14 @@ static LogFilterBridge shared() { */ private final AtomicReference armedFor = new AtomicReference<>(); + /** True from {@link #holdUntilJs} until JS first calls {@link #setEnabled}. For the watchdog only. */ + private final AtomicBoolean held = new AtomicBoolean(); + + /** Whether the startup hold is still waiting for JS to call {@code setLogFilter}. */ + boolean isHeld() { + return held.get(); + } + /** Production: one daemon thread forgets pending requests at {@link #BORROW_MS}. */ private LogFilterBridge() { this(new DaemonScheduler(), System::currentTimeMillis, Bugsee::setLogEventFilter); @@ -147,7 +155,8 @@ private LogFilterBridge() { this(scheduler, clock, filter -> { }); } - private LogFilterBridge( + /** Tests pass the call that installs the SDK's filter. */ + LogFilterBridge( @NonNull final Scheduler scheduler, @NonNull final ConsoleEchoDedup.Clock clock, @NonNull final Installer installer @@ -197,6 +206,39 @@ void attachModule(@NonNull final Sink next) { ensureInstalled(); } + /** + * Installs the filter before JS has asked for it, so a manifest + * auto-launch does not record log lines unfiltered until + * {@code setLogFilter} runs (see {@link StartupHold}). It is the state of + * an app whose filter is on but whose runtime has not armed it: lines are + * dropped. {@link #attachModule} keeps it, so it also holds while the + * module is constructed ahead of its JS. {@code setEnabled(true)} arms + * the filter, and {@code setEnabled(false)} (what {@code setLogFilter(null)} + * sends) releases it to the pass-through. + */ + void holdUntilJs() { + // userFilter first: once the filter is installed no line may pass + // through before it is on. If the install throws it is rolled back + // (the throw goes on to StartupHold, which logs it) and the hold is + // not reported, as in the other two bridges; attachModule installs + // again when the module is built. + synchronized (stateLock) { + userFilter.set(true); + } + boolean ok = false; + try { + ensureInstalled(); + ok = true; + } finally { + if (!ok) { + synchronized (stateLock) { + userFilter.set(false); + } + } + } + held.set(true); + } + /** * The module {@code caller}'s JS now answers requests. Ignored unless * {@code caller} is still the attached module: a stale module's late @@ -263,6 +305,7 @@ void setEnabled(final boolean enabled, final Sink caller) { // A superseded module must neither arm nor disable the process-wide filter. return; } + held.set(false); if (enabled && caller != null) { arm(caller); } diff --git a/packages/react-native/android/src/main/java/com/bugsee/reactnative/NetworkFilterBridge.java b/packages/react-native/android/src/main/java/com/bugsee/reactnative/NetworkFilterBridge.java index 32bf61e6..02e67a53 100644 --- a/packages/react-native/android/src/main/java/com/bugsee/reactnative/NetworkFilterBridge.java +++ b/packages/react-native/android/src/main/java/com/bugsee/reactnative/NetworkFilterBridge.java @@ -126,7 +126,7 @@ static NetworkFilterBridge shared() { /** Production: one daemon thread forgets pending requests at {@link #BORROW_MS}. */ private NetworkFilterBridge() { - this(new DaemonScheduler(), NetworkFilterBridge::readDefaultSanitizerOption); + this(new DaemonScheduler(), NetworkFilterBridge::readDefaultSanitizerOption, Bugsee::setNetworkEventFilter); } /** @@ -136,8 +136,64 @@ private NetworkFilterBridge() { * {@link Bugsee#getLaunchOptions()} needs an Android looper. */ NetworkFilterBridge(@NonNull final Scheduler scheduler, @NonNull final BooleanSupplier defaultSanitizer) { + this(scheduler, defaultSanitizer, installed -> { }); + } + + /** As above, and the call that installs (or, with null, removes) the SDK's filter. */ + NetworkFilterBridge( + @NonNull final Scheduler scheduler, + @NonNull final BooleanSupplier defaultSanitizer, + @NonNull final Consumer> installer + ) { this.scheduler = scheduler; this.defaultSanitizer = defaultSanitizer; + this.installer = installer; + } + + private final Consumer> installer; + + /** True from {@link #holdUntilJs} until JS first calls {@link #setEnabled}. For the watchdog only. */ + private final AtomicBoolean held = new AtomicBoolean(); + + /** Whether the startup hold is still waiting for JS to call {@code setNetworkFilter}. */ + boolean isHeld() { + return held.get(); + } + + /** + * Installs the filter with the SDK. A throw (the SDK not ready that early) + * leaves {@code installed} false, so a later {@code setEnabled(true)} + * installs it instead of believing it already is, and is rethrown for the + * caller to log. + */ + private void install() { + if (installed.compareAndSet(false, true)) { + boolean ok = false; + try { + installer.accept(filter); + ok = true; + } finally { + if (!ok) { + installed.set(false); + } + } + } + } + + /** + * Installs the filter before JS has asked for it, so a manifest + * auto-launch does not record network events unfiltered until + * {@code setNetworkFilter} runs (see {@link StartupHold}). With no module + * attached yet the filter drops everything, and so does one whose JS has + * not armed it (the armed gate), so nothing waits on a reply that cannot + * come. {@code setEnabled(false)}, which {@code setNetworkFilter(null)} sends, removes + * it again. A second call, or one after {@code setEnabled(true)}, is a no-op. + */ + void holdUntilJs() { + synchronized (stateLock) { + install(); + held.set(true); + } } private final EventFilter filter = new EventFilter() { @@ -221,17 +277,16 @@ void setEnabled(final boolean enabled, final Sink caller) { // A superseded module must neither arm nor disable the process-wide filter. return; } + held.set(false); if (enabled) { if (caller != null) { arm(caller); } - if (installed.compareAndSet(false, true)) { - Bugsee.setNetworkEventFilter(filter); - } + install(); return; } if (installed.compareAndSet(true, false)) { - Bugsee.setNetworkEventFilter(null); + installer.accept(null); } } } diff --git a/packages/react-native/android/src/main/java/com/bugsee/reactnative/ReactNativeWrapperInitProvider.java b/packages/react-native/android/src/main/java/com/bugsee/reactnative/ReactNativeWrapperInitProvider.java index 28cbe3af..24738472 100644 --- a/packages/react-native/android/src/main/java/com/bugsee/reactnative/ReactNativeWrapperInitProvider.java +++ b/packages/react-native/android/src/main/java/com/bugsee/reactnative/ReactNativeWrapperInitProvider.java @@ -1,8 +1,13 @@ package com.bugsee.reactnative; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.bugsee.library.BugseeExtensionInitProviderBase; @@ -29,6 +34,9 @@ * bridge is up. An early report is therefore attributed to * {@code react_native} with a correct wrapper version and an empty context, * which is strictly better than the alternative of no attribution at all. + * + *

The same early hook can close the privacy window of an auto-launched + * SDK, opt-in: see {@link StartupHold}. */ public final class ReactNativeWrapperInitProvider extends BugseeExtensionInitProviderBase { @@ -39,10 +47,50 @@ protected boolean onExtensionCreate() { // Through the registrar, like every registration: setWrapperInfo // re-registers from another thread once JS is up. registerEarly(WrapperRegistrar::register); + // Opt-in: close the window before JS installs the app's filters. + holdFiltersEarly(holdFiltersMetaData(getContext()), StartupHold.BRIDGES); // Conventional for an init provider: we expose no content. return false; } + /** + * The {@value StartupHold#META_DATA} meta-data, or null when it is absent + * or cannot be read. Never throws: see {@link #registerEarly}. + */ + @Nullable + static Object holdFiltersMetaData(@Nullable final Context context) { + try { + if (context == null) { + return null; + } + final ApplicationInfo info = context.getPackageManager() + .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + return holdFiltersValueOf(info); + } catch (Throwable t) { + Log.e(TAG, "could not read " + StartupHold.META_DATA + ": " + t.getClass().getName()); + return null; + } + } + + /** The meta-data value in {@code info}, or null. */ + @Nullable + static Object holdFiltersValueOf(@NonNull final ApplicationInfo info) { + final Bundle data = info.metaData; + return data == null ? null : data.get(StartupHold.META_DATA); + } + + /** Never throws; see {@link StartupHold#apply}. */ + static void holdFiltersEarly(@Nullable final Object metaData, @NonNull final StartupHold.Targets targets) { + try { + final java.util.Set held = StartupHold.apply(metaData, targets); + if (targets == StartupHold.BRIDGES) { + StartupHold.watchFromProvider(held); + } + } catch (Throwable t) { + Log.e(TAG, "could not hold the filters at process start: " + t.getClass().getName()); + } + } + /** * Never throws, errors included. This runs before * {@code Application.onCreate} in every host app that links the library, diff --git a/packages/react-native/android/src/main/java/com/bugsee/reactnative/StartupHold.java b/packages/react-native/android/src/main/java/com/bugsee/reactnative/StartupHold.java new file mode 100644 index 00000000..f78a24ec --- /dev/null +++ b/packages/react-native/android/src/main/java/com/bugsee/reactnative/StartupHold.java @@ -0,0 +1,237 @@ +package com.bugsee.reactnative; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; + +/** + * The opt-in that closes the window in which an auto-launched SDK records + * before JavaScript has installed the app's filters. + * + *

Under manifest auto-launch the SDK starts at process start, long before a + * JS runtime exists. The JS log, network and breadcrumb filters are installed + * natively only when {@code setLogFilter} / {@code setNetworkFilter} / + * {@code setBreadcrumbFilter} run, so everything captured until then is + * recorded unfiltered. The Android SDK keeps a filter set before launch and + * applies it to the providers when they are created, so the wrapper's init + * provider can install the bridge filters first. Each bridge's filter drops an + * event while no module (or a module whose JS has not answered) is there to + * ask: fail closed. + * + *

Opt-in, per filter, by the {@value #META_DATA} manifest meta-data (the + * Expo plugin's {@code holdFilters} option): a held filter also drops + * everything for an app that never calls its {@code setXFilter}, so the + * default is not to hold. JS releases a hold by calling the setter, with a + * function or with {@code null} (no filter, pass-through). + */ +final class StartupHold { + + static final String META_DATA = "com.bugsee.reactnative.hold-filters"; + static final String NETWORK = "network"; + static final String LOG = "log"; + static final String BREADCRUMB = "breadcrumb"; + + private static final String TAG = "BugseeRN"; + + private StartupHold() { + } + + /** How long after a module attaches a hold may wait for JS before the watchdog says so. */ + static final long WATCHDOG_MS = 30_000L; + + /** Which holds JS has not released yet. */ + interface Probe { + boolean networkHeld(); + + boolean logHeld(); + + boolean breadcrumbHeld(); + } + + /** Runs a task once, after a delay. Injectable so tests control time. */ + interface Delayer { + void runAfter(long delayMs, @NonNull Runnable task); + } + + /** The holds {@code apply} puts in place. */ + interface Targets { + void holdNetwork(); + + void holdLog(); + + void holdBreadcrumb(); + } + + /** The real probe. */ + static final Probe PROBE = new Probe() { + @Override + public boolean networkHeld() { + return NetworkFilterBridge.shared().isHeld(); + } + + @Override + public boolean logHeld() { + return LogFilterBridge.shared().isHeld(); + } + + @Override + public boolean breadcrumbHeld() { + return BreadcrumbFilterBridge.shared().isHeld(); + } + }; + + private static final java.util.concurrent.atomic.AtomicBoolean WATCHING = + new java.util.concurrent.atomic.AtomicBoolean(); + + /** The real bridges. */ + static final Targets BRIDGES = new Targets() { + @Override + public void holdNetwork() { + NetworkFilterBridge.shared().holdUntilJs(); + } + + @Override + public void holdLog() { + LogFilterBridge.shared().holdUntilJs(); + } + + @Override + public void holdBreadcrumb() { + BreadcrumbFilterBridge.shared().holdUntilJs(); + } + }; + + /** + * The filters {@code raw} names: a comma list of {@link #NETWORK}, + * {@link #LOG} and {@link #BREADCRUMB}, or {@code all} / {@code true} for + * all three. Case and spaces do not matter. A name that is not one of + * them is ignored, since a typo that held nothing is no worse than the + * default, but one that threw would kill the process at start. + */ + @NonNull + static Set parse(@Nullable final Object raw) { + if (raw == null) { + return Collections.emptySet(); + } + final Set held = new LinkedHashSet<>(); + for (final String part : raw.toString().split(",")) { + final String name = part.trim().toLowerCase(Locale.ROOT); + if (name.equals("all") || name.equals("true")) { + held.add(NETWORK); + held.add(LOG); + held.add(BREADCRUMB); + } else if (name.equals(NETWORK) || name.equals(LOG) || name.equals(BREADCRUMB)) { + held.add(name); + } + } + return held; + } + + /** + * Holds each filter {@code raw} names. One that fails to hold does not + * stop the others: the rest still close their window. Returns what was + * held. Never throws, errors included; see + * {@link ReactNativeWrapperInitProvider#registerEarly}. + */ + @NonNull + static Set apply(@Nullable final Object raw, @NonNull final Targets targets) { + final Set held = new LinkedHashSet<>(); + for (final String name : parse(raw)) { + try { + if (name.equals(NETWORK)) { + targets.holdNetwork(); + } else if (name.equals(LOG)) { + targets.holdLog(); + } else { + targets.holdBreadcrumb(); + } + held.add(name); + } catch (Throwable t) { + Log.e(TAG, "could not hold the " + name + " filter at process start: " + + t.getClass().getName()); + } + } + if (!held.isEmpty()) { + Log.i(TAG, "holding the " + held + " filter(s) from process start: events are dropped until JS calls " + + "setNetworkFilter / setLogFilter / setBreadcrumbFilter (a function, or null for none)"); + } + return held; + } + + /** + * The filters {@code probe} says JS has not released, in a fixed order. + */ + @NonNull + static Set stillHeld(@NonNull final Probe probe) { + final Set waiting = new LinkedHashSet<>(); + if (probe.networkHeld()) { + waiting.add(NETWORK); + } + if (probe.logHeld()) { + waiting.add(LOG); + } + if (probe.breadcrumbHeld()) { + waiting.add(BREADCRUMB); + } + return waiting; + } + + /** + * Once per process, {@code delayMs} after the first module attaches: says so + * (and returns what, to the log) if a filter is still held, because an + * app that lists a filter and never calls its setter records none of that + * kind. A log line only; it changes no behaviour. + * + * @return whether this call scheduled the watchdog + */ + static boolean watch( + @NonNull final Probe probe, + @NonNull final Delayer delayer, + @NonNull final java.util.function.Consumer warn, + final long delayMs + ) { + if (!WATCHING.compareAndSet(false, true)) { + return false; + } + delayer.runAfter(delayMs, () -> { + final Set waiting = stillHeld(probe); + if (!waiting.isEmpty()) { + warn.accept("the " + waiting + " filter(s) are still held " + (delayMs / 1000L) + + " s after process start: events of that kind are being dropped. If JS runs, call " + + "setNetworkFilter / setLogFilter / setBreadcrumbFilter at startup (null for no filter), " + + "or remove it from holdFilters; a process that never starts React Native holds them for its whole life"); + } + }); + return true; + } + + /** + * Starts the watchdog from the init provider, so it runs in a process that + * never starts React Native too ("still held 30 s after process start"). + * A no-op when nothing was held. + */ + static void watchFromProvider(@NonNull final Set held) { + try { + if (held.isEmpty()) { + return; + } + final Handler main = new Handler(Looper.getMainLooper()); + watch(PROBE, (delayMs, task) -> main.postDelayed(task, delayMs), message -> Log.w(TAG, message), WATCHDOG_MS); + } catch (Throwable t) { + Log.e(TAG, "could not start the startup hold watchdog: " + t.getClass().getName()); + } + } + + /** Test seam: lets another test start a fresh watchdog. */ + static void resetWatchForTest() { + WATCHING.set(false); + } +} diff --git a/packages/react-native/android/src/test/java/com/bugsee/reactnative/BreadcrumbFilterBridgeTest.java b/packages/react-native/android/src/test/java/com/bugsee/reactnative/BreadcrumbFilterBridgeTest.java index 6d8bc349..c82a9674 100644 --- a/packages/react-native/android/src/test/java/com/bugsee/reactnative/BreadcrumbFilterBridgeTest.java +++ b/packages/react-native/android/src/test/java/com/bugsee/reactnative/BreadcrumbFilterBridgeTest.java @@ -11,6 +11,7 @@ import com.bugsee.library.contracts.common.Callback1; import com.bugsee.library.contracts.exchange.Breadcrumb; +import com.bugsee.library.contracts.exchange.EventFilter; import org.json.JSONObject; import org.junit.Test; @@ -649,4 +650,117 @@ public void aStaleModulesEnableDoesNotArmTheNewModule() { assertEquals(1, next.json.size()); assertEquals(0, asked.runs); } + + // The startup hold (#134): the SDK's filter is installed before JS exists. + private final List> installs = new ArrayList<>(); + + private BreadcrumbFilterBridge installing() { + return new BreadcrumbFilterBridge(scheduler, System::nanoTime, installs::add); + } + + @Test + public void holdUntilJsInstallsTheFilterOnceAndDropsBeforeAnyModule() { + final BreadcrumbFilterBridge held = installing(); + + held.holdUntilJs(); + held.holdUntilJs(); + + assertEquals(1, installs.size()); + assertTrue(held.isHeld()); + final RecordingCallback callback = new RecordingCallback(); + installs.get(0).filter(new MutableCrumb(), callback); + assertEquals(1, callback.runs); + assertTrue(callback.sawNull); + } + + @Test + public void aHeldBridgeDropsAtOnceWhileAModuleIsAttachedButNotArmed() { + final BreadcrumbFilterBridge held = installing(); + final RecordingSink sink = new RecordingSink(); + held.holdUntilJs(); + held.attach(sink); + final RecordingCallback callback = new RecordingCallback(); + + held.ask(new MutableCrumb(), callback); + + // Nothing is left pending for a reply that cannot come. + assertTrue(sink.ids.isEmpty()); + assertTrue(callback.sawNull); + } + + @Test + public void setBreadcrumbFilterWithNullRemovesTheHeldFilter() { + final BreadcrumbFilterBridge held = installing(); + final RecordingSink sink = new RecordingSink(); + held.holdUntilJs(); + held.attach(sink); + + held.setEnabled(false, sink); + + assertFalse(held.isHeld()); + assertEquals(2, installs.size()); + assertNull(installs.get(1)); + } + + @Test + public void setBreadcrumbFilterWithAFunctionKeepsTheInstalledFilterAndArmsIt() { + final BreadcrumbFilterBridge held = installing(); + final RecordingSink sink = new RecordingSink(); + held.holdUntilJs(); + held.attach(sink); + + held.setEnabled(true, sink); + + assertFalse(held.isHeld()); + assertEquals(1, installs.size()); + held.ask(new MutableCrumb(), new RecordingCallback()); + assertEquals(1, sink.ids.size()); + } + + @Test + public void aSupersededModuleCannotReleaseTheBreadcrumbHold() { + final BreadcrumbFilterBridge held = installing(); + final RecordingSink old = new RecordingSink(); + held.holdUntilJs(); + held.attach(old); + held.attach(new RecordingSink()); + + held.setEnabled(false, old); + + assertTrue(held.isHeld()); + } + + @Test + public void aBridgeThatWasNeverHeldReportsNotHeld() { + assertFalse(installing().isHeld()); + } + + @Test + public void aFailedEarlyInstallIsRetriedByTheNextSetEnabled() { + final int[] attempts = {0}; + final java.util.function.Consumer> failingOnce = filter -> { + attempts[0]++; + if (attempts[0] == 1) { + throw new IllegalStateException("SDK not ready"); + } + installs.add(filter); + }; + final BreadcrumbFilterBridge bridge2 = new BreadcrumbFilterBridge(scheduler, System::nanoTime, failingOnce); + final RecordingSink sink = new RecordingSink(); + bridge2.attach(sink); + + try { + bridge2.holdUntilJs(); + org.junit.Assert.fail("the install failure is rethrown for StartupHold to log"); + } catch (final IllegalStateException expected) { + // StartupHold logs it and goes on. + } + // Not installed, so not reported as held. + assertFalse(bridge2.isHeld()); + + bridge2.setEnabled(true, sink); + + assertEquals(2, attempts[0]); + assertEquals(1, installs.size()); + } } diff --git a/packages/react-native/android/src/test/java/com/bugsee/reactnative/LogFilterBridgeTest.java b/packages/react-native/android/src/test/java/com/bugsee/reactnative/LogFilterBridgeTest.java index 935ba518..ecbe1007 100644 --- a/packages/react-native/android/src/test/java/com/bugsee/reactnative/LogFilterBridgeTest.java +++ b/packages/react-native/android/src/test/java/com/bugsee/reactnative/LogFilterBridgeTest.java @@ -1,6 +1,7 @@ package com.bugsee.reactnative; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -273,6 +274,137 @@ public void detachDropsALiveBorrowAndSkipsARecycledOne() { assertEquals(0, recycled.setMessageCalls); } + // A manifest auto-launch records before JS runs. A held bridge must not + // pass a line through unfiltered in that window, with or without a module. + @Test + public void aHeldBridgeDropsLinesBeforeAnyModuleIsAttached() { + bridge.holdUntilJs(); + final RecordingCallback callback = new RecordingCallback(); + + bridge.route(new MutableLog("secret"), callback); + + assertEquals(1, callback.runs); + assertTrue(callback.sawNull); + } + + @Test + public void aHeldBridgeDropsAtOnceOnceAModuleIsAttachedButNotArmed() { + final RecordingSink sink = new RecordingSink(); + bridge.holdUntilJs(); + bridge.attachModule(sink); + final RecordingCallback callback = new RecordingCallback(); + + bridge.route(new MutableLog("secret"), callback); + + assertTrue(sink.ids.isEmpty()); + assertEquals(1, callback.runs); + assertTrue(callback.sawNull); + } + + @Test + public void callingSetLogFilterWithNullReleasesTheHoldToPassThrough() { + final RecordingSink sink = new RecordingSink(); + bridge.holdUntilJs(); + bridge.attachModule(sink); + assertTrue(bridge.isHeld()); + + bridge.setEnabled(false, sink); + + assertFalse(bridge.isHeld()); + final MutableLog line = new MutableLog("public"); + final RecordingCallback callback = new RecordingCallback(); + bridge.route(line, callback); + assertEquals(1, callback.runs); + assertSame(line, callback.last); + } + + @Test + public void callingSetLogFilterWithAFunctionArmsItAndAsksJs() { + final RecordingSink sink = new RecordingSink(); + bridge.holdUntilJs(); + bridge.attachModule(sink); + + bridge.setEnabled(true, sink); + + assertFalse(bridge.isHeld()); + final RecordingCallback callback = new RecordingCallback(); + bridge.route(new MutableLog("secret"), callback); + assertEquals(1, sink.ids.size()); + assertEquals(0, callback.runs); + } + + @Test + public void aSupersededModuleCannotReleaseTheHold() { + final RecordingSink old = new RecordingSink(); + final RecordingSink next = new RecordingSink(); + bridge.holdUntilJs(); + bridge.attachModule(old); + bridge.attachModule(next); + + bridge.setEnabled(false, old); + + assertTrue(bridge.isHeld()); + final RecordingCallback callback = new RecordingCallback(); + bridge.route(new MutableLog("secret"), callback); + assertTrue(callback.sawNull); + } + + @Test + public void aReloadBeforeJsRunsStaysHeld() { + bridge.holdUntilJs(); + bridge.attachModule(new RecordingSink()); + bridge.attachModule(new RecordingSink()); + + final RecordingCallback callback = new RecordingCallback(); + bridge.route(new MutableLog("secret"), callback); + + assertTrue(bridge.isHeld()); + assertTrue(callback.sawNull); + } + + @Test + public void aFailedEarlyInstallIsNotReportedAsHeldAndIsRolledBack() { + final int[] attempts = {0}; + final LogFilterBridge failing = new LogFilterBridge(scheduler, System::currentTimeMillis, filter -> { + attempts[0]++; + if (attempts[0] == 1) { + throw new IllegalStateException("SDK not ready"); + } + }); + + try { + failing.holdUntilJs(); + org.junit.Assert.fail("the install failure is rethrown for StartupHold to log"); + } catch (final IllegalStateException expected) { + // StartupHold logs it and goes on. + } + + assertFalse(failing.isHeld()); + // Rolled back: not a silent drop-everything without a filter behind it. + final MutableLog line = new MutableLog("public"); + final RecordingCallback callback = new RecordingCallback(); + failing.route(line, callback); + assertSame(line, callback.last); + + // A retry installs and holds. + failing.holdUntilJs(); + assertTrue(failing.isHeld()); + assertEquals(2, attempts[0]); + } + + @Test + public void aBridgeThatWasNeverHeldIsThePassThrough() { + bridge.attachModule(new RecordingSink()); + assertFalse(bridge.isHeld()); + final MutableLog line = new MutableLog("public"); + final RecordingCallback callback = new RecordingCallback(); + + bridge.route(line, callback); + + assertSame(line, callback.last); + assertEquals(1, callback.runs); + } + /** * The console patch notes the echo before logcat is written. The channel * line is asked. The logcat echo is dropped without a second ask. A diff --git a/packages/react-native/android/src/test/java/com/bugsee/reactnative/NetworkFilterBridgeTest.java b/packages/react-native/android/src/test/java/com/bugsee/reactnative/NetworkFilterBridgeTest.java index dea49718..f20e26df 100644 --- a/packages/react-native/android/src/test/java/com/bugsee/reactnative/NetworkFilterBridgeTest.java +++ b/packages/react-native/android/src/test/java/com/bugsee/reactnative/NetworkFilterBridgeTest.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import com.bugsee.library.contracts.common.Callback1; +import com.bugsee.library.contracts.exchange.EventFilter; import com.bugsee.library.contracts.exchange.NetworkEvent; import com.bugsee.library.contracts.options.Options; import com.bugsee.library.contracts.options.OptionsContainer; @@ -520,4 +521,120 @@ public void aStaleModulesEnableDoesNotArmTheNewModule() { assertEquals(1, next.ids.size()); assertEquals(0, asked.runs); } + + // The startup hold (#134): the SDK's filter is installed before JS exists. + private final List> installs = new ArrayList<>(); + + private NetworkFilterBridge installing() { + return new NetworkFilterBridge(scheduler, () -> false, installs::add); + } + + @Test + public void holdUntilJsInstallsTheFilterOnceAndDropsBeforeAnyModule() { + final NetworkFilterBridge held = installing(); + + held.holdUntilJs(); + held.holdUntilJs(); + + assertEquals(1, installs.size()); + assertTrue(installs.get(0) != null); + assertTrue(held.isHeld()); + final RecordingCallback callback = new RecordingCallback(); + installs.get(0).filter(new MutableNetwork("https://secret.example/token"), callback); + assertEquals(1, callback.runs); + assertTrue(callback.sawNull); + } + + @Test + public void aHeldBridgeDropsAtOnceWhileAModuleIsAttachedButNotArmed() { + final NetworkFilterBridge held = installing(); + final RecordingSink sink = new RecordingSink(); + held.holdUntilJs(); + held.attach(sink); + final RecordingCallback callback = new RecordingCallback(); + + held.ask(new MutableNetwork("https://secret.example/token"), callback); + + assertTrue(sink.ids.isEmpty()); + assertTrue(callback.sawNull); + assertTrue(scheduler.tasks.isEmpty()); + } + + @Test + public void setNetworkFilterWithNullRemovesTheHeldFilter() { + final NetworkFilterBridge held = installing(); + final RecordingSink sink = new RecordingSink(); + held.holdUntilJs(); + held.attach(sink); + + held.setEnabled(false, sink); + + assertFalse(held.isHeld()); + assertEquals(2, installs.size()); + assertNull(installs.get(1)); + } + + @Test + public void setNetworkFilterWithAFunctionKeepsTheInstalledFilterAndArmsIt() { + final NetworkFilterBridge held = installing(); + final RecordingSink sink = new RecordingSink(); + held.holdUntilJs(); + held.attach(sink); + + held.setEnabled(true, sink); + + assertFalse(held.isHeld()); + assertEquals(1, installs.size()); + final RecordingCallback callback = new RecordingCallback(); + held.ask(new MutableNetwork("https://secret.example/token"), callback); + assertEquals(1, sink.ids.size()); + } + + @Test + public void aSupersededModuleCannotReleaseTheNetworkHold() { + final NetworkFilterBridge held = installing(); + final RecordingSink old = new RecordingSink(); + held.holdUntilJs(); + held.attach(old); + held.attach(new RecordingSink()); + + held.setEnabled(false, old); + + assertTrue(held.isHeld()); + assertEquals(1, installs.size()); + } + + @Test + public void aBridgeThatWasNeverHeldReportsNotHeld() { + assertFalse(installing().isHeld()); + } + + @Test + public void aFailedEarlyInstallIsRetriedByTheNextSetEnabled() { + final int[] attempts = {0}; + final java.util.function.Consumer> failingOnce = filter -> { + attempts[0]++; + if (attempts[0] == 1) { + throw new IllegalStateException("SDK not ready"); + } + installs.add(filter); + }; + final NetworkFilterBridge bridge2 = new NetworkFilterBridge(scheduler, () -> false, failingOnce); + final RecordingSink sink = new RecordingSink(); + bridge2.attach(sink); + + try { + bridge2.holdUntilJs(); + org.junit.Assert.fail("the install failure is rethrown for StartupHold to log"); + } catch (final IllegalStateException expected) { + // StartupHold logs it and goes on. + } + // Not installed, so not reported as held. + assertFalse(bridge2.isHeld()); + + bridge2.setEnabled(true, sink); + + assertEquals(2, attempts[0]); + assertEquals(1, installs.size()); + } } diff --git a/packages/react-native/android/src/test/java/com/bugsee/reactnative/ReactNativeWrapperInitProviderTest.java b/packages/react-native/android/src/test/java/com/bugsee/reactnative/ReactNativeWrapperInitProviderTest.java index ce7aa063..c01fbb0e 100644 --- a/packages/react-native/android/src/test/java/com/bugsee/reactnative/ReactNativeWrapperInitProviderTest.java +++ b/packages/react-native/android/src/test/java/com/bugsee/reactnative/ReactNativeWrapperInitProviderTest.java @@ -42,4 +42,67 @@ public void swallowsAnErrorFromRegistration() { throw new NoClassDefFoundError("com/bugsee/library/Bugsee"); }); } + + @Test + public void holdsTheFiltersTheMetaDataNames() { + final List held = new ArrayList<>(); + + ReactNativeWrapperInitProvider.holdFiltersEarly("log,breadcrumb", new StartupHold.Targets() { + @Override + public void holdNetwork() { + held.add("network"); + } + + @Override + public void holdLog() { + held.add("log"); + } + + @Override + public void holdBreadcrumb() { + held.add("breadcrumb"); + } + }); + + assertEquals(java.util.Arrays.asList("log", "breadcrumb"), held); + } + + @Test + public void anApplicationWithoutMetaDataHoldsNothing() { + final android.content.pm.ApplicationInfo info = new android.content.pm.ApplicationInfo(); + info.metaData = null; + org.junit.Assert.assertNull(ReactNativeWrapperInitProvider.holdFiltersValueOf(info)); + } + + @Test + public void aContextWhosePackageManagerThrowsReadsAsNoMetaData() { + // A ContextWrapper over nothing: getPackageManager() dereferences the null base. + final android.content.Context broken = new android.content.ContextWrapper(null); + org.junit.Assert.assertNull(ReactNativeWrapperInitProvider.holdFiltersMetaData(broken)); + } + + @Test + public void readsNoMetaDataFromAMissingContext() { + org.junit.Assert.assertNull(ReactNativeWrapperInitProvider.holdFiltersMetaData(null)); + } + + @Test + public void anErrorFromEveryHoldDoesNotEscape() { + ReactNativeWrapperInitProvider.holdFiltersEarly("all", new StartupHold.Targets() { + @Override + public void holdNetwork() { + throw new NoClassDefFoundError("com/bugsee/library/Bugsee"); + } + + @Override + public void holdLog() { + throw new IllegalStateException(); + } + + @Override + public void holdBreadcrumb() { + throw new OutOfMemoryError(); + } + }); + } } diff --git a/packages/react-native/android/src/test/java/com/bugsee/reactnative/StartupHoldTest.java b/packages/react-native/android/src/test/java/com/bugsee/reactnative/StartupHoldTest.java new file mode 100644 index 00000000..3e2ddbc0 --- /dev/null +++ b/packages/react-native/android/src/test/java/com/bugsee/reactnative/StartupHoldTest.java @@ -0,0 +1,172 @@ +package com.bugsee.reactnative; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; + +/** + * The startup hold runs in an init provider, before {@code Application.onCreate}. + * It must hold exactly what the manifest names, and never throw. + */ +public class StartupHoldTest { + + private static final class RecordingTargets implements StartupHold.Targets { + final List calls = new ArrayList<>(); + String failOn; + + private void call(final String name) { + calls.add(name); + if (name.equals(failOn)) { + throw new NoClassDefFoundError("com/bugsee/library/Bugsee"); + } + } + + @Override + public void holdNetwork() { + call("network"); + } + + @Override + public void holdLog() { + call("log"); + } + + @Override + public void holdBreadcrumb() { + call("breadcrumb"); + } + } + + @Test + public void nothingIsHeldWithoutTheMetaData() { + assertEquals(Collections.emptySet(), StartupHold.parse(null)); + assertEquals(Collections.emptySet(), StartupHold.parse("")); + assertEquals(Collections.emptySet(), StartupHold.parse(" , ")); + final RecordingTargets targets = new RecordingTargets(); + assertTrue(StartupHold.apply(null, targets).isEmpty()); + assertTrue(targets.calls.isEmpty()); + } + + @Test + public void aListHoldsOnlyTheFiltersItNames() { + assertEquals(new LinkedHashSet<>(Arrays.asList("log")), StartupHold.parse("log")); + assertEquals(new LinkedHashSet<>(Arrays.asList("network", "breadcrumb")), + StartupHold.parse(" Network , BREADCRUMB,network")); + } + + @Test + public void allAndTrueHoldEveryFilter() { + final LinkedHashSet everything = new LinkedHashSet<>(Arrays.asList("network", "log", "breadcrumb")); + assertEquals(everything, StartupHold.parse("all")); + assertEquals(everything, StartupHold.parse("true")); + assertEquals(everything, StartupHold.parse(Boolean.TRUE)); + assertEquals(Collections.emptySet(), StartupHold.parse(Boolean.FALSE)); + } + + @Test + public void anUnknownNameIsIgnoredNotFatal() { + assertEquals(new LinkedHashSet<>(Arrays.asList("log")), StartupHold.parse("logs,log,secure")); + assertEquals(Collections.emptySet(), StartupHold.parse("logs")); + } + + @Test + public void applyHoldsEachNamedFilterOnce() { + final RecordingTargets targets = new RecordingTargets(); + + final java.util.Set held = StartupHold.apply("log,network,log", targets); + + assertEquals(Arrays.asList("log", "network"), targets.calls); + assertEquals(new LinkedHashSet<>(Arrays.asList("log", "network")), held); + } + + @Test + public void aFailingHoldDoesNotStopTheOthersAndIsNotReportedAsHeld() { + final RecordingTargets targets = new RecordingTargets(); + targets.failOn = "network"; + + final java.util.Set held = StartupHold.apply("all", targets); + + assertEquals(Arrays.asList("network", "log", "breadcrumb"), targets.calls); + assertEquals(new LinkedHashSet<>(Arrays.asList("log", "breadcrumb")), held); + } + + private static final class FakeProbe implements StartupHold.Probe { + boolean network; + boolean log; + boolean breadcrumb; + + @Override + public boolean networkHeld() { + return network; + } + + @Override + public boolean logHeld() { + return log; + } + + @Override + public boolean breadcrumbHeld() { + return breadcrumb; + } + } + + @Test + public void stillHeldListsTheUnreleasedFiltersInAFixedOrder() { + final FakeProbe probe = new FakeProbe(); + assertTrue(StartupHold.stillHeld(probe).isEmpty()); + probe.breadcrumb = true; + probe.network = true; + assertEquals(new LinkedHashSet<>(Arrays.asList("network", "breadcrumb")), StartupHold.stillHeld(probe)); + probe.log = true; + assertEquals(new LinkedHashSet<>(Arrays.asList("network", "log", "breadcrumb")), StartupHold.stillHeld(probe)); + } + + @Test + public void theWatchdogWarnsOnceAfterTheDelayOnlyForFiltersStillHeld() { + StartupHold.resetWatchForTest(); + final FakeProbe probe = new FakeProbe(); + probe.log = true; + final List scheduled = new ArrayList<>(); + final List delays = new ArrayList<>(); + final List warnings = new ArrayList<>(); + + assertTrue(StartupHold.watch(probe, (delay, task) -> { + delays.add(delay); + scheduled.add(task); + }, warnings::add, 30_000L)); + // A second module (a reload) does not start a second watchdog. + assertFalse(StartupHold.watch(probe, (delay, task) -> scheduled.add(task), warnings::add, 30_000L)); + + assertEquals(Arrays.asList(30_000L), delays); + assertEquals(1, scheduled.size()); + assertTrue(warnings.isEmpty()); + scheduled.get(0).run(); + assertEquals(1, warnings.size()); + assertTrue(warnings.get(0).contains("[log]")); + assertTrue(warnings.get(0).contains("30 s")); + StartupHold.resetWatchForTest(); + } + + @Test + public void theWatchdogSaysNothingWhenJsReleasedEveryHold() { + StartupHold.resetWatchForTest(); + final FakeProbe probe = new FakeProbe(); + final List scheduled = new ArrayList<>(); + final List warnings = new ArrayList<>(); + StartupHold.watch(probe, (delay, task) -> scheduled.add(task), warnings::add, 1_000L); + + scheduled.get(0).run(); + + assertTrue(warnings.isEmpty()); + StartupHold.resetWatchForTest(); + } +} diff --git a/packages/react-native/plugin/build/index.d.ts b/packages/react-native/plugin/build/index.d.ts index ade76ca6..a9dbb639 100644 --- a/packages/react-native/plugin/build/index.d.ts +++ b/packages/react-native/plugin/build/index.d.ts @@ -1,4 +1,5 @@ import type { ConfigPlugin } from '@expo/config-plugins'; +import type { HoldableFilter } from './manifest'; export type AppTokenOption = string | { ios?: string; android?: string; @@ -83,6 +84,20 @@ export interface BugseePluginProps { * one of these again. */ endpoint?: string; + /** + * Android only; defaults off. Keeps the JS log, network and breadcrumb + * filters fail closed from process start. With `autoLaunch` the SDK records + * before JavaScript has run, so until `setLogFilter` / `setNetworkFilter` / + * `setBreadcrumbFilter` installs the app's filter, everything is recorded + * unfiltered. `true` holds all three; a list holds those named. A held + * filter drops its events (log lines, network events, breadcrumbs) until JS + * calls that `setXFilter`, with a function (filtering) or `null` (no filter, + * pass-through). An app that never calls one therefore records none of that + * kind, so only list the filters the app installs. Meant for `autoLaunch`: + * not verified with a JS `launch()`. + * A later prebuild without it removes the meta-data again. + */ + holdFilters?: boolean | readonly HoldableFilter[]; } /** The Info.plist key the iOS native module reads `endpoint` from. */ export declare const IOS_ENDPOINT_PLIST_KEY = "BugseeReactNativeEndpoint"; diff --git a/packages/react-native/plugin/build/index.js b/packages/react-native/plugin/build/index.js index e829c774..7f30e822 100644 --- a/packages/react-native/plugin/build/index.js +++ b/packages/react-native/plugin/build/index.js @@ -77,6 +77,7 @@ const withBugsee = (config, props) => { const editGradleFiles = options.editGradleFiles !== false; const symbolicateReactNative = options.symbolicateReactNativeFrameworks === true; const endpoint = (0, endpoint_1.pluginEndpoint)(options.endpoint); + const holdFilters = (0, manifest_1.holdFiltersValue)(options.holdFilters); const versions = (0, native_versions_1.loadNativeVersions)(__dirname); const edits = { gradlePluginVersion: options.gradlePluginVersion ?? versions.gradlePlugin, @@ -138,6 +139,10 @@ const withBugsee = (config, props) => { if (edit.warning) { console.warn(edit.warning); } + config_plugins_1.AndroidConfig.Manifest.removeMetaDataItemFromMainApplication(mainApplication, manifest_1.RN_HOLD_FILTERS_META_DATA); + if (holdFilters !== null) { + config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, manifest_1.RN_HOLD_FILTERS_META_DATA, holdFilters); + } return cfg; }); config = (0, config_plugins_1.withXcodeProject)(config, (cfg) => { diff --git a/packages/react-native/plugin/build/index.js.map b/packages/react-native/plugin/build/index.js.map index e766f6cf..e1121297 100644 --- a/packages/react-native/plugin/build/index.js.map +++ b/packages/react-native/plugin/build/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/react-native/plugin/build/manifest.d.ts b/packages/react-native/plugin/build/manifest.d.ts index e948ac52..bb80942e 100644 --- a/packages/react-native/plugin/build/manifest.d.ts +++ b/packages/react-native/plugin/build/manifest.d.ts @@ -66,3 +66,20 @@ export declare const SDK_ENDPOINT_KEPT: string; * endpoint behind, and one with a new value replaces the old one. */ export declare function endpointMetaData(input: EndpointMetaDataInput): EndpointMetaDataEdit; +/** + * The wrapper's own meta-data for the startup filter hold: a comma list of + * the filters the Android native module installs, fail closed, in its init + * provider, before the SDK can launch. See `ReactNativeWrapperInitProvider`. + * The name is the wrapper's, so a value found under it is this plugin's and is + * replaced or removed freely. + */ +export declare const RN_HOLD_FILTERS_META_DATA = "com.bugsee.reactnative.hold-filters"; +export declare const HOLDABLE_FILTERS: readonly ["network", "log", "breadcrumb"]; +export type HoldableFilter = (typeof HOLDABLE_FILTERS)[number]; +export declare const HOLD_FILTERS_NOT_VALID: string; +/** + * The `holdFilters` option as the meta-data value, or null when none is + * written (unset, `false` or an empty list). `true` holds all three. Anything + * else is refused, so a typo cannot silently leave a filter open. + */ +export declare function holdFiltersValue(option: unknown): string | null; diff --git a/packages/react-native/plugin/build/manifest.js b/packages/react-native/plugin/build/manifest.js index 9a3a3f5a..b709d464 100644 --- a/packages/react-native/plugin/build/manifest.js +++ b/packages/react-native/plugin/build/manifest.js @@ -1,9 +1,10 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.SDK_ENDPOINT_KEPT = exports.SDK_ENDPOINT_MARKER = exports.SDK_ENDPOINT_META_DATA = exports.RN_ENDPOINT_META_DATA = exports.APP_TOKEN_MARKER = exports.APP_TOKEN_META_DATA = void 0; +exports.HOLD_FILTERS_NOT_VALID = exports.HOLDABLE_FILTERS = exports.RN_HOLD_FILTERS_META_DATA = exports.SDK_ENDPOINT_KEPT = exports.SDK_ENDPOINT_MARKER = exports.SDK_ENDPOINT_META_DATA = exports.RN_ENDPOINT_META_DATA = exports.APP_TOKEN_MARKER = exports.APP_TOKEN_META_DATA = void 0; exports.manifestAutoLaunchToken = manifestAutoLaunchToken; exports.appTokenMetaData = appTokenMetaData; exports.endpointMetaData = endpointMetaData; +exports.holdFiltersValue = holdFiltersValue; const properties_1 = require("./properties"); /** * The `com.bugsee.app-token` meta-data value, or null when the manifest @@ -96,4 +97,39 @@ function endpointMetaData(input) { set[exports.SDK_ENDPOINT_MARKER] = input.endpoint; return { remove, set }; } +/** + * The wrapper's own meta-data for the startup filter hold: a comma list of + * the filters the Android native module installs, fail closed, in its init + * provider, before the SDK can launch. See `ReactNativeWrapperInitProvider`. + * The name is the wrapper's, so a value found under it is this plugin's and is + * replaced or removed freely. + */ +exports.RN_HOLD_FILTERS_META_DATA = 'com.bugsee.reactnative.hold-filters'; +exports.HOLDABLE_FILTERS = ['network', 'log', 'breadcrumb']; +exports.HOLD_FILTERS_NOT_VALID = `@bugsee/react-native: holdFilters must be true, false, or a list of ${exports.HOLDABLE_FILTERS.join(', ')}`; +/** + * The `holdFilters` option as the meta-data value, or null when none is + * written (unset, `false` or an empty list). `true` holds all three. Anything + * else is refused, so a typo cannot silently leave a filter open. + */ +function holdFiltersValue(option) { + if (option === undefined || option === null || option === false) { + return null; + } + if (option === true) { + return exports.HOLDABLE_FILTERS.join(','); + } + if (!Array.isArray(option)) { + throw new Error(exports.HOLD_FILTERS_NOT_VALID); + } + const held = new Set(); + for (const name of option) { + if (!exports.HOLDABLE_FILTERS.includes(name)) { + throw new Error(exports.HOLD_FILTERS_NOT_VALID); + } + held.add(name); + } + const ordered = exports.HOLDABLE_FILTERS.filter((name) => held.has(name)); + return ordered.length === 0 ? null : ordered.join(','); +} //# sourceMappingURL=manifest.js.map \ No newline at end of file diff --git a/packages/react-native/plugin/build/manifest.js.map b/packages/react-native/plugin/build/manifest.js.map index ca746c00..538a277d 100644 --- a/packages/react-native/plugin/build/manifest.js.map +++ b/packages/react-native/plugin/build/manifest.js.map @@ -1 +1 @@ -{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":";;;AAaA,0DASC;AAuBD,4CAeC;AA6CD,4CAyBC;AAlID,6CAAkD;AAQlD;;;;GAIG;AACH,SAAgB,uBAAuB,CAAC,KAAsB;IAC5D,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;IACnC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,IAAA,+BAAkB,EAAC,KAAK,CAAC,EAAE,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACU,QAAA,mBAAmB,GAAG,sBAAsB,CAAC;AAC7C,QAAA,gBAAgB,GAAG,kCAAkC,CAAC;AASnE;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,KAA4B;IAC3D,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,wBAAgB,CAAC,CAAC;IAChD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,wBAAgB,CAAC,CAAC;QAC9B,IAAI,KAAK,CAAC,QAAQ,CAAC,2BAAmB,CAAC,KAAK,MAAM,EAAE,CAAC;YACnD,MAAM,CAAC,IAAI,CAAC,2BAAmB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;QACzB,GAAG,CAAC,2BAAmB,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;QACvC,GAAG,CAAC,wBAAgB,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;IACtC,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AACzB,CAAC;AAED;;;;;GAKG;AACU,QAAA,qBAAqB,GAAG,iCAAiC,CAAC;AAEvE;;;;GAIG;AACU,QAAA,sBAAsB,GAAG,8BAA8B,CAAC;AACxD,QAAA,mBAAmB,GAAG,qCAAqC,CAAC;AAoB5D,QAAA,iBAAiB,GAC5B,0DAA0D,8BAAsB,0BAA0B;IAC1G,mGAAmG,CAAC;AAEtG;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,KAA4B;IAC3D,MAAM,MAAM,GAAa,CAAC,6BAAqB,CAAC,CAAC;IACjD,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,2BAAmB,CAAC,CAAC;IACnD,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC,8BAAsB,CAAC,CAAC;IACnD,MAAM,SAAS,GAAG,MAAM,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,CAAC;IACzD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,2BAAmB,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,CAAC,IAAI,CAAC,8BAAsB,CAAC,CAAC;IACtC,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACzB,CAAC;IACD,GAAG,CAAC,6BAAqB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC;IAC5C,IAAI,KAAK,CAAC,eAAe,KAAK,IAAI,EAAE,CAAC;QACnC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACzB,CAAC;IACD,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;QACpC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,yBAAiB,EAAE,CAAC;IACrD,CAAC;IACD,GAAG,CAAC,8BAAsB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC;IAC7C,GAAG,CAAC,2BAAmB,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC;IAC1C,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AACzB,CAAC"} \ No newline at end of file +{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/react-native/plugin/src/__tests__/with-bugsee.test.ts b/packages/react-native/plugin/src/__tests__/with-bugsee.test.ts index a6d56070..23d2e6ec 100644 --- a/packages/react-native/plugin/src/__tests__/with-bugsee.test.ts +++ b/packages/react-native/plugin/src/__tests__/with-bugsee.test.ts @@ -8,7 +8,7 @@ import type { ExpoConfig } from '@expo/config-types'; import withBugsee, { APP_GRADLE_NOT_GROOVY, gradleNotGroovy, platformToken } from '../index'; import { ENDPOINT_HAS_VERSION, ENDPOINT_NOT_VALID } from '../endpoint'; -import { SDK_ENDPOINT_KEPT } from '../manifest'; +import { HOLD_FILTERS_NOT_VALID, SDK_ENDPOINT_KEPT, holdFiltersValue } from '../manifest'; import type { BugseePluginProps } from '../index'; import { CANNOT_EDIT } from '../gradle'; import type * as NativeVersions from '../native-versions'; @@ -263,6 +263,61 @@ describe('withBugsee through the Expo mod compiler', () => { expect(read('manifest')).toContain(`android:value="${ANDROID_TOKEN}"`); }); + describe('holdFiltersValue', () => { + it.each([ + [undefined, null], + [null, null], + [false, null], + [[], null], + [true, 'network,log,breadcrumb'], + [['log'], 'log'], + [['breadcrumb', 'log', 'log'], 'log,breadcrumb'], + [['network', 'log', 'breadcrumb'], 'network,log,breadcrumb'], + ])('%j is %j', (option, expected) => { + expect(holdFiltersValue(option)).toBe(expected); + }); + + it.each([['network'], [0], [1], [{}], [['Log']], [['network', 'x']], [[null]], [''], [[['log']]]])( + 'refuses %j', + (option) => { + expect(() => holdFiltersValue(option)).toThrow(HOLD_FILTERS_NOT_VALID); + }, + ); + }); + + describe('holdFilters', () => { + const HOLD_META = 'android:name="com.bugsee.reactnative.hold-filters"'; + + it('writes nothing when it is unset, false or empty', async () => { + for (const holdFilters of [undefined, false, []] as const) { + await prebuild({ appToken: ANDROID_TOKEN, holdFilters }); + expect(read('manifest')).not.toContain('hold-filters'); + } + }); + + it('holds all three for true, and the named ones in a fixed order for a list', async () => { + await prebuild({ appToken: ANDROID_TOKEN, holdFilters: true }); + expect(read('manifest')).toContain(``); + await prebuild({ appToken: ANDROID_TOKEN, holdFilters: ['breadcrumb', 'network', 'network'] }); + expect(read('manifest')).toContain(``); + expect(read('manifest').split(HOLD_META)).toHaveLength(2); + }); + + it('is written without autoLaunch, and removed again byte for byte', async () => { + await prebuild({ appToken: ANDROID_TOKEN }); + const without = snapshot(projectRoot); + await prebuild({ appToken: ANDROID_TOKEN, holdFilters: ['log'] }); + expect(read('manifest')).toContain(`${HOLD_META} android:value="log"`); + expect(read('manifest')).not.toContain('com.bugsee.app-token'); + await prebuild({ appToken: ANDROID_TOKEN }); + expect(snapshot(projectRoot)).toEqual(without); + }); + + it.each([[['logs']], ['network'], [1], [[1]]])('fails the prebuild for %j', async (holdFilters) => { + await expect(prebuild({ holdFilters } as unknown as BugseePluginProps)).rejects.toThrow(HOLD_FILTERS_NOT_VALID); + }); + }); + it('removes the manifest token a later prebuild no longer writes', async () => { await prebuild({ appToken: ANDROID_TOKEN, autoLaunch: true }); expect(read('manifest')).toContain('android:name="com.bugsee.app-token"'); diff --git a/packages/react-native/plugin/src/index.ts b/packages/react-native/plugin/src/index.ts index 15aec9c2..9d3792c3 100644 --- a/packages/react-native/plugin/src/index.ts +++ b/packages/react-native/plugin/src/index.ts @@ -28,7 +28,14 @@ import { ensureMavenCentral, ensureSymbolUploads, } from './gradle'; -import { appTokenMetaData, endpointMetaData, manifestAutoLaunchToken } from './manifest'; +import { + RN_HOLD_FILTERS_META_DATA, + appTokenMetaData, + endpointMetaData, + holdFiltersValue, + manifestAutoLaunchToken, +} from './manifest'; +import type { HoldableFilter } from './manifest'; import { loadNativeVersions } from './native-versions'; import { insertSymbolicatePrebuilt, removeSymbolicatePrebuilt } from './podfile'; import { bugseePropertiesText } from './properties'; @@ -116,6 +123,20 @@ export interface BugseePluginProps { * one of these again. */ endpoint?: string; + /** + * Android only; defaults off. Keeps the JS log, network and breadcrumb + * filters fail closed from process start. With `autoLaunch` the SDK records + * before JavaScript has run, so until `setLogFilter` / `setNetworkFilter` / + * `setBreadcrumbFilter` installs the app's filter, everything is recorded + * unfiltered. `true` holds all three; a list holds those named. A held + * filter drops its events (log lines, network events, breadcrumbs) until JS + * calls that `setXFilter`, with a function (filtering) or `null` (no filter, + * pass-through). An app that never calls one therefore records none of that + * kind, so only list the filters the app installs. Meant for `autoLaunch`: + * not verified with a JS `launch()`. + * A later prebuild without it removes the meta-data again. + */ + holdFilters?: boolean | readonly HoldableFilter[]; } /** The Info.plist key the iOS native module reads `endpoint` from. */ @@ -204,6 +225,7 @@ const withBugsee: ConfigPlugin = (config, props) => { const editGradleFiles = options.editGradleFiles !== false; const symbolicateReactNative = options.symbolicateReactNativeFrameworks === true; const endpoint = pluginEndpoint(options.endpoint); + const holdFilters = holdFiltersValue(options.holdFilters); const versions = loadNativeVersions(__dirname); const edits: GradleEdits = { gradlePluginVersion: options.gradlePluginVersion ?? versions.gradlePlugin, @@ -274,6 +296,10 @@ const withBugsee: ConfigPlugin = (config, props) => { if (edit.warning) { console.warn(edit.warning); } + AndroidConfig.Manifest.removeMetaDataItemFromMainApplication(mainApplication, RN_HOLD_FILTERS_META_DATA); + if (holdFilters !== null) { + AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, RN_HOLD_FILTERS_META_DATA, holdFilters); + } return cfg; }); diff --git a/packages/react-native/plugin/src/manifest.ts b/packages/react-native/plugin/src/manifest.ts index 06491803..22965b38 100644 --- a/packages/react-native/plugin/src/manifest.ts +++ b/packages/react-native/plugin/src/manifest.ts @@ -129,3 +129,44 @@ export function endpointMetaData(input: EndpointMetaDataInput): EndpointMetaData set[SDK_ENDPOINT_MARKER] = input.endpoint; return { remove, set }; } + +/** + * The wrapper's own meta-data for the startup filter hold: a comma list of + * the filters the Android native module installs, fail closed, in its init + * provider, before the SDK can launch. See `ReactNativeWrapperInitProvider`. + * The name is the wrapper's, so a value found under it is this plugin's and is + * replaced or removed freely. + */ +export const RN_HOLD_FILTERS_META_DATA = 'com.bugsee.reactnative.hold-filters'; + +export const HOLDABLE_FILTERS = ['network', 'log', 'breadcrumb'] as const; +export type HoldableFilter = (typeof HOLDABLE_FILTERS)[number]; + +export const HOLD_FILTERS_NOT_VALID = + `@bugsee/react-native: holdFilters must be true, false, or a list of ${HOLDABLE_FILTERS.join(', ')}`; + +/** + * The `holdFilters` option as the meta-data value, or null when none is + * written (unset, `false` or an empty list). `true` holds all three. Anything + * else is refused, so a typo cannot silently leave a filter open. + */ +export function holdFiltersValue(option: unknown): string | null { + if (option === undefined || option === null || option === false) { + return null; + } + if (option === true) { + return HOLDABLE_FILTERS.join(','); + } + if (!Array.isArray(option)) { + throw new Error(HOLD_FILTERS_NOT_VALID); + } + const held = new Set(); + for (const name of option) { + if (!(HOLDABLE_FILTERS as readonly unknown[]).includes(name)) { + throw new Error(HOLD_FILTERS_NOT_VALID); + } + held.add(name as string); + } + const ordered = HOLDABLE_FILTERS.filter((name) => held.has(name)); + return ordered.length === 0 ? null : ordered.join(','); +} diff --git a/packages/react-native/src/__tests__/lifecycle.test.ts b/packages/react-native/src/__tests__/lifecycle.test.ts index caae352b..141c07f2 100644 --- a/packages/react-native/src/__tests__/lifecycle.test.ts +++ b/packages/react-native/src/__tests__/lifecycle.test.ts @@ -265,6 +265,25 @@ describe('attach', () => { expect(setCrashDetectionOptedOut).toHaveBeenLastCalledWith(true); }); + // An auto-launched session registered only type and version natively, at + // process start; the context is knowable in JS alone. + it('registers the full wrapper identity, as launch does', async () => { + await Bugsee.attach(); + expect(native.setWrapperInfo).toHaveBeenCalledTimes(1); + const identity = native.setWrapperInfo.mock.calls[0]?.[0]; + expect(identity).toEqual( + expect.objectContaining({ type: expect.any(String), version: expect.any(String), context: expect.any(Object) }), + ); + await Bugsee.launch('tok'); + expect(native.setWrapperInfo.mock.calls[1]?.[0]).toEqual(identity); + }); + + it('registers the wrapper before it reads the launch options, so a failed read still attributes', async () => { + native.getLaunchOptions.mockRejectedValue(new Error('options unavailable')); + await expect(Bugsee.attach()).rejects.toThrow(/options unavailable/); + expect(native.setWrapperInfo).toHaveBeenCalledTimes(1); + }); + it('installs console capture from the launch options it just read', async () => { const opts = { 'com.bugsee.option.capture.logs': false }; native.getLaunchOptions.mockResolvedValue(opts); diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index de3edab1..a3234c70 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -155,7 +155,7 @@ class Bugsee { /** * Tells the SDK what wrapper it is running under. * - * Idempotent and cheap, so `launch` and `relaunch` can both call it; the + * Idempotent and cheap, so `launch`, `relaunch` and `attach` can all call it; the * SDK replaces whatever was registered. */ private registerWrapper(): void { @@ -701,6 +701,11 @@ class Bugsee { * from `com.bugsee.app-token` manifest metadata. Deliberately makes no * native launch call; doing so would start a second session. * + * Registers the full wrapper identity first, as `launch` and `relaunch` + * do: the native side registered only the type and version at process + * start, so without this every report of an auto-launched session lacks + * the context (React Native version, JS engine, dev build). + * * Reads `com.bugsee.option.detect.crash` from the native launch options and * applies it before the global handlers are installed (R7). Capture is held * off during that read: the default is on, and the read yields, so a render @@ -710,6 +715,7 @@ class Bugsee { * handlers; a rejected read does not install it either. */ async attach(): Promise { + this.registerWrapper(); setExceptionCaptureEnabled(false); const options = await this.getLaunchOptions(); applyExceptionCaptureFromOptions(options);