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
2 changes: 1 addition & 1 deletion docs/design/plans/2026-09-16-implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -3666,7 +3666,7 @@ FOUNDATION_EXPORT NSArray<NSString *> *_Nullable BGSRNStringArray(NSArray *_Null

Both methods run inside `BGSRNRunOnMain`.
- `upload:description:severity:labels:` → `[Bugsee uploadWithSummary:… description:… severity:(BugseeSeverityLevel)BGSRNUploadSeverity(severity, [Bugsee getLaunchOptions]) labels:BGSRNStringArray(labels)]`.
- `showReportDialog:…` → `[Bugsee showReportDialog]` when every argument is absent (`nil`, `nil`, `0`, `nil`). Otherwise → `showReportDialogWithSummary:(summary ?: @"") description:(description ?: @"") severity:(BugseeSeverityLevel)severity labels:BGSRNStringArray(labels)`.
- `showReportDialog:…` → `[Bugsee showReportDialog]` when every argument is absent (`nil`, `nil`, `0`, `nil`). Otherwise → `showReportDialogWithSummary:summary description:description severity:(BugseeSeverityLevel)severity labels:BGSRNStringArray(labels)`. An omitted summary or description stays `nil`. beta3 writes the field only when the pointer is non-nil, and `@""` would pre-fill an empty string.

- [x] **Red**
- `upload.test.ts`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import android.util.Log;

import com.bugsee.library.Bugsee;
import com.bugsee.library.contracts.options.IssueSeverity;
import com.bugsee.library.contracts.reporting.Report;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
Expand All @@ -16,6 +17,7 @@
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;

Expand Down Expand Up @@ -362,12 +364,56 @@ public void logUnhandledException(final String payloadJson, final Promise promis
});
}

// The two-argument overload only -- severity and labels are Phase 8. JS
// has already checked both arguments are strings, so nothing is
// re-validated here.
/**
* JS has already checked the strings, the severity range and the labels.
* {@code severity} 0 and a null label list are the SDK default. A throw
* from the SDK must not escape a void method.
*/
@Override
public void upload(final String summary, final String description) {
Bugsee.upload(summary, description);
public void upload(
final String summary,
final String description,
final double severity,
@Nullable final ReadableArray labels
) {
try {
Bugsee.upload(
summary,
description,
ReportArgs.severity((int) severity),
ReportArgs.labels(labels == null ? null : labels.toArrayList()));
} catch (final RuntimeException e) {
Log.e(TAG, "upload failed", e);
}
}

/**
* On the UI thread: the dialog presents from there. Same argument
* mapping as {@link #upload}. Does nothing before launch -- the SDK
* logs and returns. The dialog runs {@code onBeforeReportCreated}
* before it opens.
*/
@Override
public void showReportDialog(
@Nullable final String summary,
@Nullable final String description,
final double severity,
@Nullable final ReadableArray labels
) {
try {
final IssueSeverity sev = ReportArgs.severity((int) severity);
final ArrayList<String> labelList =
ReportArgs.labels(labels == null ? null : labels.toArrayList());
UiThreadUtil.runOnUiThread(() -> {
try {
Bugsee.showReportDialog(summary, description, sev, labelList);
} catch (final RuntimeException e) {
Log.e(TAG, "showReportDialog failed", e);
}
});
} catch (final RuntimeException e) {
Log.e(TAG, "showReportDialog failed", e);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.bugsee.reactnative;

import android.util.Log;

import androidx.annotation.Nullable;

import com.bugsee.library.contracts.options.IssueSeverity;

import java.util.ArrayList;
import java.util.List;

/**
* Turns the numbers and lists JS sends for a manual report into the types
* the Android SDK expects.
*
* <p>Severity is the enum's internal value, never its ordinal.
* {@code IssueSeverity.fromIntValue} maps anything outside 1..5 to
* {@code VeryLow}, so the range is checked here first: {@code 0} and anything
* else become null and the SDK applies its own default. Only a value outside
* that sentinel is logged; {@code 0} is the wire's way of saying "default".
*/
final class ReportArgs {

private static final String TAG = "BugseeRN";

private ReportArgs() {
}

/** 0 and anything outside 1..5 are null (the SDK default). 1..5 map by value. */
@Nullable
static IssueSeverity severity(final int wire) {
if (wire == 0) {
return null;
}
if (wire >= 1 && wire <= 5) {
return IssueSeverity.fromIntValue(wire);
}
Log.w(TAG, "severity " + wire + " is outside 1..5; the SDK default applies");
return null;
}

/** null stays null. Anything that is not a string is dropped. */
@Nullable
static ArrayList<String> labels(@Nullable final List<?> wire) {
if (wire == null) {
return null;
}
final ArrayList<String> result = new ArrayList<>();
for (final Object item : wire) {
if (item instanceof String) {
result.add((String) item);
}
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.bugsee.reactnative;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import com.bugsee.library.contracts.options.IssueSeverity;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* Severity crosses as the enum's internal value, never its ordinal.
* {@code values()[4]} is Blocker; value 4 is Critical.
*/
public class ReportArgsTest {

@Test
public void zeroIsTheSdkDefault() {
assertNull(ReportArgs.severity(0));
}

@Test
public void severityIsByValueNotOrdinal() {
assertEquals(IssueSeverity.Critical, ReportArgs.severity(4));
assertEquals(IssueSeverity.VeryLow, ReportArgs.severity(1));
assertEquals(IssueSeverity.Blocker, IssueSeverity.values()[4]);
assertEquals(IssueSeverity.Medium, IssueSeverity.values()[1]);
}

@Test
public void outOfRangeIsTheDefault() {
assertNull(ReportArgs.severity(6));
assertNull(ReportArgs.severity(-1));
assertNull(ReportArgs.severity(99));
}

@Test
public void labelsBecomeAnArrayList() {
final ArrayList<String> labels = ReportArgs.labels(Arrays.asList("a", "b"));
assertEquals(Arrays.asList("a", "b"), labels);
assertTrue(labels instanceof ArrayList);
}

@Test
public void nullLabelsStayNull() {
assertNull(ReportArgs.labels(null));
}

@Test
public void nonStringLabelsAreDropped() {
final List<Object> mixed = new ArrayList<>();
mixed.add("keep");
mixed.add(1);
mixed.add(null);
mixed.add("also");
assertEquals(Arrays.asList("keep", "also"), ReportArgs.labels(mixed));
}
}
44 changes: 38 additions & 6 deletions packages/react-native/ios/BugseeModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#import <BugseeRNSupport/BGSRNReactWindow.h>
#import <BugseeRNSupport/BGSRNExceptions.h>
#import <BugseeRNSupport/BGSRNSettleOnce.h>
#import <BugseeRNSupport/BGSRNReportArgs.h>
#else
#import "BGSRNMainThread.h"
#import "BGSRNWrapper.h"
Expand All @@ -47,6 +48,7 @@
#import "BGSRNReactWindow.h"
#import "BGSRNExceptions.h"
#import "BGSRNSettleOnce.h"
#import "BGSRNReportArgs.h"
#endif

/// The conformance lives here rather than in the Support package so that the
Expand Down Expand Up @@ -608,14 +610,44 @@ - (void)logUnhandledException:(NSString *)payloadJson
});
}

/// The two-argument form only -- severity and labels are Phase 8. JS has
/// already checked both arguments are strings. On main, like every other SDK
/// entry point; the report it creates is a LIVE one, so its handlers run on
/// main too, after this returns.
/// JS has already checked both strings, the severity range and the labels.
/// Severity 0 is resolved here: the requested value, else the launch option
/// `BugseeOptionReportingDefaultBugPriority`, else High. On main, like every
/// other SDK entry point. Does nothing before launch -- the SDK returns.
/// The report it creates is a LIVE one, so its handlers run on main too,
/// after this returns.
- (void)upload:(NSString *)summary
description:(NSString *)description {
description:(NSString *)description
severity:(double)severity
labels:(NSArray *)labels {
BGSRNRunOnMain(^{
[Bugsee uploadWithSummary:summary description:description];
[Bugsee uploadWithSummary:summary
description:description
severity:(BugseeSeverityLevel)BGSRNUploadSeverity((NSInteger)severity,
[Bugsee getLaunchOptions])
labels:BGSRNStringArray(labels)];
});
}

/// Every argument absent (`nil`, `nil`, `0`, `nil`) calls the no-argument
/// dialog. Otherwise an omitted summary or description stays `nil`: beta3
/// writes the field only when the pointer is non-nil, and `@""` would
/// pre-fill an empty string. A 0 severity is left for the SDK to skip.
/// On main. Does nothing before launch. The dialog runs
/// `onBeforeReportCreated` before it opens.
- (void)showReportDialog:(NSString *)summary
description:(NSString *)description
severity:(double)severity
labels:(NSArray *)labels {
BGSRNRunOnMain(^{
if (summary == nil && description == nil && severity == 0.0 && labels == nil) {
[Bugsee showReportDialog];
return;
}
[Bugsee showReportDialogWithSummary:summary
description:description
severity:(BugseeSeverityLevel)severity
labels:BGSRNStringArray(labels)];
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#import "include/BGSRNReportArgs.h"

#import <Bugsee/Bugsee.h>

const NSInteger BGSRNDefaultBugPriorityFallback = 3;

static BOOL BGSRNIntegralSeverity(id raw, NSInteger *out) {
if (![raw isKindOfClass:[NSNumber class]]) {
return NO;
}
// A boolean is an NSNumber whose integer value is 0 or 1. It is not a
// severity the launch option was given as.
if (CFGetTypeID((__bridge CFTypeRef)raw) == CFBooleanGetTypeID()) {
return NO;
}
const double value = [(NSNumber *)raw doubleValue];
if (value < 1.0 || value > 5.0 || value != (NSInteger)value) {
return NO;
}
*out = (NSInteger)value;
return YES;
}

NSInteger BGSRNUploadSeverity(NSInteger requested, NSDictionary *_Nullable launchOptions) {
if (requested >= 1 && requested <= 5) {
return requested;
}
NSInteger option = 0;
id raw = launchOptions[BugseeOptionReportingDefaultBugPriority];
if (BGSRNIntegralSeverity(raw, &option)) {
return option;
}
return BGSRNDefaultBugPriorityFallback;
}

NSArray<NSString *> *_Nullable BGSRNStringArray(NSArray *_Nullable raw) {
if (raw == nil) {
return nil;
}
NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:raw.count];
for (id item in raw) {
if ([item isKindOfClass:[NSString class]]) {
[result addObject:item];
}
}
return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/// High (`BugseeSeverityHigh`, 3). Used when neither the requested severity
/// nor the launch option is an integer 1..5.
FOUNDATION_EXPORT const NSInteger BGSRNDefaultBugPriorityFallback;

/// `requested` when it is 1..5; otherwise
/// `launchOptions[BugseeOptionReportingDefaultBugPriority]` when that value
/// is an integer 1..5; otherwise `BGSRNDefaultBugPriorityFallback`.
FOUNDATION_EXPORT NSInteger BGSRNUploadSeverity(NSInteger requested, NSDictionary *_Nullable launchOptions);

/// nil for nil; the NSString elements otherwise. Anything else is dropped.
FOUNDATION_EXPORT NSArray<NSString *> *_Nullable BGSRNStringArray(NSArray *_Nullable raw);

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@import XCTest;
@import Bugsee;
@import BugseeRNSupport;

/// Upload severity is the requested value when it is 1..5, otherwise the
/// launch option, otherwise High (3). A nil string array stays nil.
@interface BGSRNReportArgsTests : XCTestCase
@end

@implementation BGSRNReportArgsTests

- (void)testARequestedSeverityWins {
NSDictionary *options = @{BugseeOptionReportingDefaultBugPriority : @(BugseeSeverityLow)};
XCTAssertEqual(BGSRNUploadSeverity(BugseeSeverityCritical, options), BugseeSeverityCritical);
XCTAssertEqual(BGSRNUploadSeverity(BugseeSeverityLow, nil), BugseeSeverityLow);
XCTAssertEqual(BGSRNDefaultBugPriorityFallback, 3);
}

- (void)testZeroTakesTheLaunchOption {
NSDictionary *options =
@{BugseeOptionReportingDefaultBugPriority : @(BugseeSeverityCritical)};
XCTAssertEqual(BGSRNUploadSeverity(0, options), BugseeSeverityCritical);
}

- (void)testZeroWithoutTheOptionIsHigh {
XCTAssertEqual(BGSRNUploadSeverity(0, @{}), BugseeSeverityHigh);
XCTAssertEqual(BGSRNUploadSeverity(0, nil), BugseeSeverityHigh);
}

- (void)testAnOutOfRangeOptionIsHigh {
XCTAssertEqual(BGSRNUploadSeverity(0, @{BugseeOptionReportingDefaultBugPriority : @(6)}),
BugseeSeverityHigh);
XCTAssertEqual(BGSRNUploadSeverity(0, @{BugseeOptionReportingDefaultBugPriority : @(0)}),
BugseeSeverityHigh);
XCTAssertEqual(BGSRNUploadSeverity(0, @{BugseeOptionReportingDefaultBugPriority : @(2.5)}),
BugseeSeverityHigh);
XCTAssertEqual(BGSRNUploadSeverity(0, @{BugseeOptionReportingDefaultBugPriority : @"4"}),
BugseeSeverityHigh);
}

- (void)testStringArrayKeepsStrings {
NSArray *mixed = @[ @"a", @1, @"b" ];
XCTAssertEqualObjects(BGSRNStringArray(mixed), (@[ @"a", @"b" ]));
XCTAssertEqualObjects(BGSRNStringArray(@[ @"only" ]), (@[ @"only" ]));
}

- (void)testStringArrayOfNilIsNil {
XCTAssertNil(BGSRNStringArray(nil));
}

@end
Loading
Loading