Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/design/2026-09-15-sdk-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Verified during Phase 3, on-device and by reading both SDKs' sources. `bugsee/sp

**Android.** A crash the SDK recovers through its own bounded early-recovery dispatch — a call off the live `BugseeReportHandlerThread`, completion `Callback.NOOP` — completes natively at once (`completed by=recovery`) and never reaches JS; the bridge detects this by thread name, not by `isTerminating` (the plan's Phase 3 rulings). A crash recovered at the *next launch*, when that launch is JS-initiated, is not one path. A Java uncaught exception is dispatched on the live `BugseeReportHandlerThread` with the ordinary 25 s live deadline, so it does reach JS and `onAfterReportCreated`'s edits land in the retained crash bundle. Verified on the WOD_LX1 (Task 3.4d): the relaunch logged `deadline=25000` and `completed by=js`, never `by=recovery`. An NDK crash (SIGSEGV or SIGABRT) recovered at that same JS-initiated launch is the bounded path instead: the SDK dispatches it on `bugsee-report-handler-bounded`, the bridge completes `by=recovery`, and the JS handler is never asked. Verified on the WOD_LX1 (Task 7.6b): the relaunch logged `completed by=recovery phase=after` and never `by=js`.

**iOS.** Live dispatch is on the main thread (25 s deadline); a report recovered at relaunch is dispatched off main (2.5 s deadline) — and, unlike Android's bounded early-recovery path, it **does** reach JS, because iOS re-persists the report on a late completion until the bundle is assembled. The recovery case cannot run in the Simulator: the simulator slice of 7.0.0-beta3 has no crash reporter (Task 3.4f), so it is gated `E2E_IOS_RECOVERY=1` and proven only on physical hardware (§12).
**iOS.** Live dispatch is on the main thread (25 s deadline); a report recovered at relaunch is dispatched off main (2.5 s deadline) — and, unlike Android's bounded early-recovery path, it **does** reach JS, because iOS re-persists the report on a late completion until the bundle is assembled. The recovery case cannot run in the Simulator: the simulator slice of 7.0.0-beta3 has no crash reporter (Task 3.4f), and neither has 7.0.0-beta4's (no PLCrashReporter symbol in `ios-arm64_x86_64-simulator`), so it is gated `E2E_IOS_RECOVERY=1` and proven only on physical hardware (§12).

**iOS threading, load-bearing for both cases.** The SDK invokes the live report handler with `dispatch_async` onto main; its completion is a thread-agnostic run-once that hops to a private queue. Main is never blocked waiting for it (`BGSIssueReportingCoordinator.m` on `nextgen`). Consequently the bridge's own report ops and its call to `completeReportHandler` must never hop to main themselves — there is no need to, and queuing behind whatever UI work is already on main would eat into the handle's deadline for nothing. The bridge completes from background queues throughout.

Expand Down Expand Up @@ -159,7 +159,7 @@ Typed `EventEmitter<T>` members (available in codegen from RN 0.76, so safe at a

- `s.platforms` reads React Native's own `min_ios_version_supported` (15.1 from
RN 0.76 onward) rather than a literal, so the pod tracks the app's RN
version. As of `7.0.0-beta3` the SDK's own deployment target is 15.0 (raised
version. As of `7.0.0-beta4` the SDK's own deployment target is 15.0 (raised
in beta2, from the 13.0 this section originally recorded), so the two floors
now agree — see §4.5 and `platform-floors.ts`.
- **No `s.dependency 'Bugsee'`** — no pod exists, and none ever will.
Expand Down Expand Up @@ -518,7 +518,7 @@ Work outside this repository that this design depends on or has surfaced.
> of 2026-09-15. Phase 3 of the plan (rulings and *Verified facts*,
> 2026-09-28) superseded them: Android moved to `7.3.0` (pinned transitionally
> as `7.3.0-SNAPSHOT`), the Gradle plugin to `4.0.7`, and iOS to `7.0.0-beta3`
> with a 15.0 deployment target. Read this appendix for the shape of the
> with a 15.0 deployment target; iOS then moved to `7.0.0-beta4` (2026-10-06). Read this appendix for the shape of the
> verification (what was checked and how), not for the version numbers
> themselves.

Expand Down
66 changes: 16 additions & 50 deletions examples/bare/e2e/attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,15 @@
* own check independently say it should not -- the iOS SDK team has been
* asked why, and the row is omitted rather than asserted either way.
*
* **iOS SDK regression, confirmed and fixed upstream:**
* https://github.com/bugsee/bugsee-cocoa/pull/164 (base `nextgen`; there is
* no separate issue -- the PR is the record). Root cause: nextgen lacked
* Android's `initializeReport`, so global attributes and the identifier were
* never copied into a new report (`BGSManifestCreator.userAttributes` is
* legacy and unused on this path). So a live `Bugsee.upload()` report's
* `manifest.json` `attrs` and `request.json` `email` never carry the global
* attributes or user identifier at all on iOS 7.0.0-beta3, even though
* `getAttribute`/`getAllAttributes`/`getUserIdentifier` all read them back
* correctly right up to the `upload()` call. Case 3's `manifest.attrs`/
* `email` assertions therefore run as `it.failing` on iOS only, so the suite
* stays green while asserting the CORRECT (currently unmet) behaviour, and
* turns red -- forcing an update -- once the RN pin moves to an iOS beta
* containing bugsee-cocoa#164. Whether a bundle was retained at all, and
* whether it's this run's own report, is asserted separately in a plain
* `it` that is never `.failing` -- so a harness regression (no bundle
* pulled, or the wrong one) fails loudly instead of being swallowed as "the
* known SDK bug failing as expected".
* **iOS SDK regression, fixed in 7.0.0-beta4:**
* https://github.com/bugsee/bugsee-cocoa/pull/164 (`fe857ad97`). nextgen
* lacked Android's `initializeReport`, so on 7.0.0-beta3 a live
* `Bugsee.upload()` report's `manifest.json` `attrs` and `request.json`
* `email` never carried the global attributes or user identifier. Case 3
* was `it.failing` on iOS until the pin moved to beta4, where it passed on
* the simulator (2026-10-06) and became a plain `it` on both platforms.
* Whether a bundle was retained at all, and whether it's this run's own
* report, is still asserted separately, before case 3.
*
* Android preconditions, as for data.test.ts: the debug build is installed on
* the handset named in device.ts, and Metro is running with
Expand Down Expand Up @@ -439,12 +430,9 @@ describeDevice(`attributes and identity round-trip on ${TARGET_NAME}`, () => {

/**
* `bundles[0]` only -- deliberately asserts nothing. Exactly one bundle,
* and that it's this run's own report, is asserted once, in a plain `it`
* that is never `.failing` (below): if this helper itself threw and were
* called from inside `it.failing`'s case 3, a harness regression (no
* bundle pulled, a stale one from an earlier run) would be swallowed as
* "the known SDK bug failing as expected" and the suite would stay green
* for the wrong reason.
* and that it's this run's own report, is asserted once, in its own `it`
* (below), so a harness regression (no bundle pulled, a stale one from an
* earlier run) fails under its own name rather than as case 3.
*/
function theBundle(): PulledBundle {
return bundles[0]!;
Expand Down Expand Up @@ -497,33 +485,11 @@ describeDevice(`attributes and identity round-trip on ${TARGET_NAME}`, () => {
});

/**
* Confirmed iOS SDK regression, fixed upstream in
* https://github.com/bugsee/bugsee-cocoa/pull/164 (base `nextgen`; there
* is no separate issue -- the PR is the record). Root cause: nextgen
* lacked Android's `initializeReport`, so global attributes and the
* identifier were never copied into a new report --
* `BGSManifestCreator.userAttributes` is legacy and unused on this path.
* So on iOS 7.0.0-beta3, a live `Bugsee.upload()` report's
* `manifest.json` `attrs` comes back `{}` and `request.json` has no
* `email` key, even though `getAttribute`/`getAllAttributes`/
* `getUserIdentifier` all read them back correctly right up to the
* `upload()` call (case 1 above).
*
* `it.failing` (a Jest built-in): the body below asserts the CORRECT
* behaviour -- unweakened, identical in shape to Android's -- and this
* test passes exactly because those assertions currently fail on iOS.
* Remove `.failing` when fixed, i.e. once the RN pin moves to an iOS beta
* containing bugsee-cocoa#164. Android runs the same body as a normal
* `it`, since it has no such bug.
*
* ONLY the two SDK-bug assertions (`manifest.attrs` contents and
* `request.json` `email`) live in this block -- the bundle's existence
* and identity are already asserted above, in the plain `it` that
* precedes this one, precisely so this `.failing` cannot mask a harness
* regression as "the known SDK bug failing as expected".
* Case 3. On iOS 7.0.0-beta3 this was `it.failing` (bugsee-cocoa#164: a
* new report did not start from the global attributes and identifier);
* 7.0.0-beta4 carries the fix, so both platforms run it as a plain `it`.
*/
const case3 = ON_IOS ? it.failing : it;
case3('the retained report carries the attributes and the identifier', () => {
it('the retained report carries the attributes and the identifier', () => {
assertPrecondition();
const bundle = theBundle();
expect(bundle.manifest.attrs).toStrictEqual(expectedAttrs(nonce));
Expand Down
59 changes: 38 additions & 21 deletions examples/bare/e2e/exceptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@
* still shows RN's JavascriptException, but exactly one Bugsee bundle is filed.
* On iOS cases 9 and 12 the unhandled report is stored, not sent: a plain
* `it` asserts the bundle list is empty, then `terminateIosApp()` and
* `exc-observe`. Exactly one recovered crash is `it.failing` on beta3
* `0d9c9d0a-9`, which does not claim `override_report.plcrash` on the next
* launch. The harness must not copy that file onto `live_report.plcrash`.
* The Release gate is the same split: the console ended and no
* `RCTFatalException` bundle on a plain `it`; exactly one
* `ReactNativeWebException` crash is `it.failing`.
* `exc-observe`, which must recover exactly one crash. beta3 `0d9c9d0a-9`
* did not claim `override_report.plcrash` on the next launch, so those
* assertions were `it.failing` there; 7.0.0-beta4 claims it (bugsee-cocoa
* #177), though intermittently on hardware (see the cases), and they are
* plain `it`. The harness must not copy that file onto
* `live_report.plcrash`. The Release gate is the same: the console ended,
* no `RCTFatalException` bundle, and exactly one `ReactNativeWebException`
* crash.
*/
import { writeFileSync } from 'node:fs';

Expand Down Expand Up @@ -1070,11 +1072,16 @@ describeDevice(`JS exceptions on ${TARGET_NAME}`, () => {
expect(completed.index).toBeLessThan(appHandler.index);
});

// beta3 0d9c9d0a-9 does not claim override_report.plcrash on the next
// launch, so this list is empty. The harness must not copy that file
// onto live_report.plcrash. The body asserts the correct outcome;
// remove .failing when a beta recovers the stored report.
itIosDevice.failing(
// beta3 0d9c9d0a-9 never claimed override_report.plcrash on the next
// launch, so this was `.failing` there. 7.0.0-beta4 claims it ahead of
// the live crash (bugsee-cocoa #177), but not every time: on KRSFT
// (2026-10-06) 2 of 5 relaunches left the file on disk and dispatched
// nothing, the SDK's version gate (+hasVersionMatchForPendingReport)
// being the lead, reported to the iOS SDK team. This asserts the correct
// outcome and can be red on a device run for that reason; it does not
// relaunch again, which would hide the miss. The harness still must not
// copy that file onto live_report.plcrash.
itIosDevice(
'a fatal JS error is reported as a crash, then RN\'s handler runs',
() => {
const crashes = bundlesWithReason(bundles, `E2E fatal ${nonce}`).filter(
Expand Down Expand Up @@ -1204,11 +1211,16 @@ describeDevice(`JS exceptions on ${TARGET_NAME}`, () => {
expect(stored).toEqual([]);
});

// beta3 0d9c9d0a-9 does not claim override_report.plcrash on the next
// launch, so this list is empty. The harness must not copy that file
// onto live_report.plcrash. The body asserts the correct outcome;
// remove .failing when a beta recovers the stored report.
itIosDevice.failing(
// beta3 0d9c9d0a-9 never claimed override_report.plcrash on the next
// launch, so this was `.failing` there. 7.0.0-beta4 claims it ahead of
// the live crash (bugsee-cocoa #177), but not every time: on KRSFT
// (2026-10-06) 2 of 5 relaunches left the file on disk and dispatched
// nothing, the SDK's version gate (+hasVersionMatchForPendingReport)
// being the lead, reported to the iOS SDK team. This asserts the correct
// outcome and can be red on a device run for that reason; it does not
// relaunch again, which would hide the miss. The harness still must not
// copy that file onto live_report.plcrash.
itIosDevice(
'a render error with no boundary is reported once, as a crash, by the root reporter',
() => {
const crashes = bundlesWithReason(bundles, `E2E boundary ${nonce}`).filter(
Expand Down Expand Up @@ -1330,11 +1342,16 @@ describeDevice(`JS exceptions on ${TARGET_NAME}`, () => {
}
});

// beta3 0d9c9d0a-9 does not claim override_report.plcrash on the next
// launch, so this list is empty. The harness must not copy that file
// onto live_report.plcrash. The body asserts the correct outcome;
// remove .failing when a beta recovers the stored report.
itIosDevice.failing('exactly one ReactNativeWebException crash for the fatal', () => {
// beta3 0d9c9d0a-9 never claimed override_report.plcrash on the next
// launch, so this was `.failing` there. 7.0.0-beta4 claims it ahead of
// the live crash (bugsee-cocoa #177), but not every time: on KRSFT
// (2026-10-06) 2 of 5 relaunches left the file on disk and dispatched
// nothing, the SDK's version gate (+hasVersionMatchForPendingReport)
// being the lead, reported to the iOS SDK team. This asserts the correct
// outcome and can be red on a device run for that reason; it does not
// relaunch again, which would hide the miss. The harness still must not
// copy that file onto live_report.plcrash.
itIosDevice('exactly one ReactNativeWebException crash for the fatal', () => {
const ours = bundles.filter(b => {
if (b.request.type !== 'crash') {
return false;
Expand Down
2 changes: 1 addition & 1 deletion examples/bare/e2e/report-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ import {
const itAndroid = ON_ANDROID ? it : it.skip;
/**
* Case 6 on iOS needs a crash reporter, and the simulator slice of the iOS SDK
* has none: 7.0.0-beta3's `ios-arm64_x86_64-simulator` binary carries no
* has none: 7.0.0-beta3's and 7.0.0-beta4's `ios-arm64_x86_64-simulator` binaries carry no
* BGSCrashManager or PLCrashReporter symbols (the SDK compiles crash hooks
* out under TARGET_OS_SIMULATOR), so a crash there is never recovered and the
* case fails at its first recovery assertion (Task 3.4f report). It runs only
Expand Down
31 changes: 29 additions & 2 deletions examples/bare/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,29 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- BugseeReactNativeFeedback (0.0.0):
- BugseeReactNative
- hermes-engine
- RCTRequired
- RCTTypeSafety
- React-bridging
- React-Core
- React-Core-prebuilt
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- DoubleConversion (1.1.6):
- ReactNativeDependencies
- fast_float (8.0.0):
Expand Down Expand Up @@ -1657,6 +1680,7 @@ DEPENDENCIES:
- boost (from `build/rndeps-facades/boost`)
- BugseeE2ENative (from `../node_modules/bugsee-e2e-native`)
- "BugseeReactNative (from `../node_modules/@bugsee/react-native`)"
- "BugseeReactNativeFeedback (from `../node_modules/@bugsee/react-native-feedback`)"
- DoubleConversion (from `build/rndeps-facades/DoubleConversion`)
- fast_float (from `build/rndeps-facades/fast_float`)
- FBLazyVector (from `build/rncore-facades/FBLazyVector`)
Expand Down Expand Up @@ -1750,6 +1774,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/bugsee-e2e-native"
BugseeReactNative:
:path: "../node_modules/@bugsee/react-native"
BugseeReactNativeFeedback:
:path: "../node_modules/@bugsee/react-native-feedback"
DoubleConversion:
:path: build/rndeps-facades/DoubleConversion
fast_float:
Expand Down Expand Up @@ -1923,7 +1949,8 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 8502bfdc46e2804408af2e163049ece181009af4
BugseeE2ENative: b4cd280a78bae9185d1a391769ececb30b0eb846
BugseeReactNative: 684066f42a96c3dcd61e50cc6bf38397738f679f
BugseeReactNative: ac04523adf1a6e3a445d84f3a7656d980e4d28ab
BugseeReactNativeFeedback: f55b2673ed02f3299b6801e154a3256ed5aa19f0
DoubleConversion: 501a575c40c0b50c89685762dd3e415e6bb26fb0
fast_float: 7c925d1cebf8328d4018d061f49377e9f887e3b4
FBLazyVector: e08a27053b503eeb756ea4bd75d570d2f2522a1b
Expand Down Expand Up @@ -2011,4 +2038,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 8b6aa5135b91f56aaf82cda5788ba89799309b83

COCOAPODS: 1.16.2
COCOAPODS: 1.17.0
3 changes: 2 additions & 1 deletion packages/react-native-feedback/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ published surface:
appearance.backgroundColor = '#112233';
```

The `feedback-spm` 7.0.0-beta3 SwiftUI chat hard-codes `Color.accentColor`
The `feedback-spm` 7.0.0-beta4 SwiftUI chat (its sources are unchanged
from 7.0.0-beta3) hard-codes `Color.accentColor`
and `Color.gray` and does not read `BugseeTheme`, so that assignment does
not change the chat in this beta. The setter still stores the color on
`BugseeTheme`.
2 changes: 1 addition & 1 deletion packages/react-native-feedback/ios/BugseeFeedbackModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ - (void)emitSentMessage:(NSString *)message;
static NSSet<NSString *> *keys;
static dispatch_once_t once;
dispatch_once(&once, ^{
// BugseeTheme.h in Bugsee 7.0.0-beta3. The SwiftUI chat in this beta
// BugseeTheme.h in Bugsee 7.0.0-beta4 (unchanged from beta3). The SwiftUI chat in this beta
// does not read them; they are still the appearance API the header
// publishes, and the only one the feedback package can set.
keys = [NSSet setWithArray:@[
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-feedback/src/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import NativeBugseeFeedback from './NativeBugseeFeedback';
*
* Android keys are the `FeedbackAppearance` constant values in
* `bugsee-android-feedback` 7.3.0 (`Feedback::ActionBarColor`, …). iOS keys
* are `BugseeTheme` properties in the 7.0.0-beta3 `Bugsee.xcframework`
* are `BugseeTheme` properties in the 7.0.0-beta4 `Bugsee.xcframework`
* header. A name that exists on only one platform has only that side; setting
* it on the other throws, so a color that will not be applied is not stored
* as if it had been.
Expand Down
5 changes: 3 additions & 2 deletions packages/react-native/BugseeReactNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ Pod::Spec.new do |s|
# helper in scripts/react_native_pods.rb, which every RN Podfile requires,
# so it is in scope by the time CocoaPods evaluates this podspec; the
# literal is the fallback for evaluation outside an app (`pod spec lint`).
# Bugsee itself supports 15.0 and below -- down to 13.0 -- but React-Core
# does not, so an app can never actually sit lower than this.
# Bugsee itself floors at 15.0 (since 7.0.0-beta2; the bugsee/spm manifest
# declares .iOS(.v15)), just below React Native's 15.1, so React Native's is
# the floor that binds and an app can never sit lower than this.
s.platforms = {
:ios => defined?(min_ios_version_supported) ? min_ios_version_supported : '15.1'
}
Expand Down
Loading
Loading