Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cd150e7
feat(android): report JS exceptions as ReactNativeWebException, kept …
krassx Sep 30, 2026
0b7c404
test(e2e): JS exceptions in Android bundles, handled, unhandled and f…
krassx Sep 30, 2026
56deb41
test(e2e): a release fatal is one ReactNativeWebException bundle
krassx Sep 30, 2026
4b75281
test(e2e): register debug IDs before a render throw
krassx Sep 30, 2026
1705c19
feat(ios): report JS exceptions as ReactNativeWebException
krassx Sep 30, 2026
3275093
test(ios): count concurrent settle with an atomic so a double-settle …
krassx Sep 30, 2026
5667fa5
test(e2e): JS exceptions in iOS bundles, on the iPhone
krassx Oct 1, 2026
fb9449b
test(e2e): pin iOS unhandled recovery as failing on beta3
krassx Oct 1, 2026
c437703
feat(report): upload with severity and labels, and showReportDialog
krassx Oct 1, 2026
cd79431
feat(report): createReport, a report the app fills and uploads, bridg…
krassx Oct 1, 2026
38bfadc
feat(ios): createReport, with the SDK's attachment limits enforced up…
krassx Oct 1, 2026
a05df5a
fix(ios): ignore a created-report callback after its reservation ended
krassx Oct 1, 2026
5f1dfa0
fix(ios): keep the created-report slot until upload finishes
krassx Oct 1, 2026
81ce130
fix(ios): keep an in-flight created-report upload across clear
krassx Oct 1, 2026
2bcf21d
fix(ios): reserve a created report before the main hop
krassx Oct 1, 2026
a88e5ea
fix(ios): hold the created-report slot until an in-flight create fini…
krassx Oct 1, 2026
555d33e
test(report): pin severity bounds and the exact argument errors
krassx Oct 1, 2026
a7657d0
test(ios): keep the settle-once race to the two callers
krassx Oct 1, 2026
7f748a4
test(report): cover created-report attachment and native rejection paths
krassx Oct 1, 2026
3db1950
test(ios): keep each settle-once window on its own release
krassx Oct 1, 2026
601eded
Merge main into this branch
krassx Oct 1, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;

/**
* The Android half of the `Bugsee` TurboModule.
Expand Down
175 changes: 146 additions & 29 deletions packages/react-native/ios/BugseeModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
#import <BugseeRNSupport/BGSRNExceptions.h>
#import <BugseeRNSupport/BGSRNSettleOnce.h>
#import <BugseeRNSupport/BGSRNReportArgs.h>
#import <BugseeRNSupport/BGSRNCreatedReports.h>
#import <BugseeRNSupport/BGSRNCreatedReportOps.h>
#else
#import "BGSRNMainThread.h"
#import "BGSRNWrapper.h"
Expand All @@ -49,6 +51,8 @@
#import "BGSRNExceptions.h"
#import "BGSRNSettleOnce.h"
#import "BGSRNReportArgs.h"
#import "BGSRNCreatedReports.h"
#import "BGSRNCreatedReportOps.h"
#endif

/// The conformance lives here rather than in the Support package so that the
Expand Down Expand Up @@ -220,6 +224,7 @@ static void BGSRNSetWrapper(id<BugseeWrapper> _Nullable wrapper, BOOL onlyIfAbse
}

static NSString *const kHandleDeadCode = @"E_REPORT_HANDLE_DEAD";
static NSString *const kCreateBusyCode = @"E_REPORT_CREATE_BUSY";

static void BGSRNRejectHandleDead(RCTPromiseRejectBlock reject) {
reject(kHandleDeadCode,
Expand All @@ -228,6 +233,10 @@ static void BGSRNRejectHandleDead(RCTPromiseRejectBlock reject) {
nil);
}

static void BGSRNRejectCreatedHandleDead(RCTPromiseRejectBlock reject) {
reject(kHandleDeadCode, @"This created report handle is no longer valid.", nil);
}

/// No code of our own (React Native fills in `EUNSPECIFIED`, as Android's
/// `promise.reject(e)` does): an unexpected fault is not one of the three the
/// JS contract names, and inventing a fourth would make it matchable.
Expand Down Expand Up @@ -388,6 +397,9 @@ - (void)invalidate {
// And answers nil to every data request this module's JS was given, and
// disables the view tree until the next runtime mounts its anchor.
[BGSRNDataRequestBridge.shared detach:self];
// The next runtime cannot know a created-report handle, and a slot left
// reserved would make every later createReport reject busy.
[BGSRNCreatedReports.shared clear];
Comment thread
krassx marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
}

/// The SDK touches UIKit during start-up, so it must not be constructed on a
Expand Down Expand Up @@ -928,70 +940,175 @@ - (void)reportAddDataAttachment:(NSString *)handleId
}
}

// Task 8.2c. No registry yet, so this does not call the SDK: the SDK made none.
/// On main, unlike the handler's report ops. `BugseeExtendedReport` is
/// unsynchronised, and the SDK hands it out and takes it back on main.
///
/// `reserve` runs before that hop. `invalidate` calls `clear` on another
/// queue and cannot cancel a block already queued. Reserving inside the
/// block would run after that clear and hold the slot for a handle the
/// torn-down JS will never upload.
- (void)createReport:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
(void)reject;
resolve(nil);
BGSRNCreatedReports *registry = BGSRNCreatedReports.shared;
const NSUInteger reservation = [registry reserve];
if (reservation == 0) {
NSLog(@"BugseeRN created report - busy");
reject(kCreateBusyCode, @"a created report is already outstanding", nil);
return;
}
BGSRNRunOnMain(^{
// One check with the mark. `clear` between an open test and the SDK call
// would still `-init` a `BugseeExtendedReport`. NO means `clear` already
// dropped this token and freed the slot. YES means `clear` from here on
// keeps the slot until `fulfil`, and that fulfil does not publish.
if (![registry beginCreate:reservation]) {
NSLog(@"BugseeRN created report - reservation ended");
resolve(nil);
return;
}
@try {
// Captured above, before the hop. `invalidate` clears the registry
// without cancelling this completion. Once `beginCreate:` has run, that
// clear keeps the slot and this fulfil must not mint a handle.
// The completion runs on every path, with nil when the SDK is not launched.
[Bugsee createReportWithCompletion:^(BugseeExtendedReport *report) {
NSString *handle = [registry fulfil:report reservation:reservation];
if (handle == nil) {
NSLog(@"BugseeRN created report - none");
resolve(nil);
} else {
NSLog(@"BugseeRN created report %@ created", handle);
resolve(handle);
}
}];
} @catch (NSException *exception) {
[registry fulfil:nil reservation:reservation];
BGSRNRejectException(reject, exception);
}
});
}

// Task 8.2c. No created-report handle exists until the registry lands.
- (void)createdReportRead:(NSString *)handleId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
(void)handleId;
(void)resolve;
BGSRNRejectHandleDead(reject);
BGSRNRunOnMain(^{
@try {
BugseeExtendedReport *report = [BGSRNCreatedReports.shared reportFor:handleId];
if (report == nil) {
BGSRNRejectCreatedHandleDead(reject);
return;
}
resolve([BGSRNCreatedReportOps readReport:report]);
} @catch (NSException *exception) {
BGSRNRejectException(reject, exception);
}
});
}

// Task 8.2c. No created-report handle exists until the registry lands.
- (void)createdReportUpdate:(NSString *)handleId
patchJson:(NSString *)patchJson
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
(void)handleId;
(void)patchJson;
(void)resolve;
BGSRNRejectHandleDead(reject);
BGSRNRunOnMain(^{
@try {
BugseeExtendedReport *report = [BGSRNCreatedReports.shared reportFor:handleId];
if (report == nil) {
BGSRNRejectCreatedHandleDead(reject);
return;
}
NSError *error = nil;
if ([BGSRNCreatedReportOps applyPatchJSON:patchJson toReport:report error:&error]) {
resolve(nil);
} else {
reject(BGSRNReportErrorWireCode(error), error.localizedDescription, nil);
}
} @catch (NSException *exception) {
BGSRNRejectException(reject, exception);
}
});
}

// Task 8.2c. No created-report handle exists until the registry lands.
/// `mimeType` is accepted because JS sends it. `BugseeAttachment` has no type,
/// and iOS drops it (P7).
- (void)createdReportAddDataAttachment:(NSString *)handleId
base64:(NSString *)base64
name:(NSString *)name
mimeType:(NSString * _Nullable)mimeType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
(void)handleId;
(void)base64;
(void)name;
(void)mimeType;
(void)resolve;
BGSRNRejectHandleDead(reject);
BGSRNRunOnMain(^{
@try {
BugseeExtendedReport *report = [BGSRNCreatedReports.shared reportFor:handleId];
if (report == nil) {
BGSRNRejectCreatedHandleDead(reject);
return;
}
NSError *error = nil;
if ([BGSRNCreatedReportOps addData:base64 name:name toReport:report error:&error]) {
resolve(nil);
} else {
reject(BGSRNReportErrorWireCode(error), error.localizedDescription, nil);
}
} @catch (NSException *exception) {
BGSRNRejectException(reject, exception);
}
});
}

// Task 8.2c. No created-report handle exists until the registry lands. No move argument.
/// No `move`: the bytes are copied at add time. `mimeType` is ignored (P7).
- (void)createdReportAddFileAttachment:(NSString *)handleId
path:(NSString *)path
name:(NSString *)name
mimeType:(NSString * _Nullable)mimeType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
(void)handleId;
(void)path;
(void)name;
(void)mimeType;
(void)resolve;
BGSRNRejectHandleDead(reject);
BGSRNRunOnMain(^{
@try {
BugseeExtendedReport *report = [BGSRNCreatedReports.shared reportFor:handleId];
if (report == nil) {
BGSRNRejectCreatedHandleDead(reject);
return;
}
NSError *error = nil;
if ([BGSRNCreatedReportOps addFileAtPath:path name:name toReport:report error:&error]) {
resolve(nil);
} else {
reject(BGSRNReportErrorWireCode(error), error.localizedDescription, nil);
}
} @catch (NSException *exception) {
BGSRNRejectException(reject, exception);
}
});
}

// Task 8.2c. No created-report handle exists until the registry lands.
- (void)createdReportUpload:(NSString *)handleId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
(void)handleId;
(void)resolve;
BGSRNRejectHandleDead(reject);
BGSRNRunOnMain(^{
NSUInteger generation = 0;
@try {
BugseeExtendedReport *report =
[BGSRNCreatedReports.shared detachForUpload:handleId generation:&generation];
if (report == nil) {
BGSRNRejectCreatedHandleDead(reject);
return;
}
// The handle is already dead. The slot stays taken until this upload
// ends: a second created report would reset beta3's file-scope attributes
// before `uploadReport:` copies them. The completion carries no success
// flag. It runs once the report is handed off, including when the SDK
// was not launched (it calls back immediately). `ok` is YES on that path.
[Bugsee uploadReport:report completion:^{
[BGSRNCreatedReports.shared endUpload:generation];
NSLog(@"BugseeRN created report %@ uploaded ok=%@", handleId, @YES);
resolve(@YES);
}];
} @catch (NSException *exception) {
[BGSRNCreatedReports.shared endUpload:generation];
BGSRNRejectException(reject, exception);
}
});
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
Expand Down
Loading
Loading