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
28 changes: 28 additions & 0 deletions examples/bare/e2e/api-arguments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,34 @@ describeDevice(`overloads and arguments on ${TARGET_NAME}`, () => {
expect(bundlesAfter.length).toBe(1);
});

/**
* The consent-withdrawal case (#142): the report is waiting for upload (dead
* endpoint), so only the finished-reports flag removes it. Android calls the
* SDK's 3-argument method; iOS its delete-everything form.
*/
it('[API-41d] deleteCollectedDataOnDevice(false, true) after stop deletes the finished report, and nothing is filed after a relaunch', async () => {
await clearBundles();
const run = await startRun('api-delete-reports');
const uploaded = await apiMarker(log!, 'delete-reports uploaded', run.scenario.nonce, 30_000, run.start);
const bundlesBefore = await listBundles();
const done = await apiMarker(log!, 'delete-reports done', run.scenario.nonce, 40_000, uploaded.index, run.start);
await new Promise(resolve => setTimeout(resolve, 2_000));
const bundlesAfter = await listBundles();
await stopApp();
// A relaunch must not find a report to file: the bundle is gone, not hidden.
const relaunch = await startRun('api-token');
await apiMarker(log!, 'token launched', relaunch.scenario.nonce, 30_000, relaunch.start);
await new Promise(resolve => setTimeout(resolve, 5_000));
const bundlesRelaunched = await listBundles();
await stopApp();
report('delete(false, true)', { done: done.text.trim(), bundlesBefore, bundlesAfter, bundlesRelaunched });
expect(jsonAfter<Settled>(done.text, 'stopped')).toEqual({ ok: true, value: true });
expect(jsonAfter<Settled>(done.text, 'deleted')).toEqual({ ok: true, value: true });
expect(bundlesBefore.length).toBe(1);
expect(bundlesAfter).toEqual([]);
expect(bundlesRelaunched).toEqual([]);
});

it('[API-31] <BugseeSecure enabled={false}> is recorded in the clear, its enabled twin masked', async () => {
const { run, bundles } = await runFor(log!, 'api-secure-off', 'secure-off uploaded', 1);
await stopApp();
Expand Down
15 changes: 15 additions & 0 deletions examples/bare/scenarios/api-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* api-report-members a handler calling every BugseeReport member the
* report-handler scenarios do not.
* api-delete-false upload, stop(), deleteCollectedDataOnDevice(false).
* api-delete-reports upload, stop(), deleteCollectedDataOnDevice(false, true).
*
* `api-boundary` and `api-secure-off` render (scenarios/api.tsx).
*/
Expand Down Expand Up @@ -47,6 +48,7 @@ export const ARGS_SCENARIOS = [
'api-handler-null',
'api-report-members',
'api-delete-false',
'api-delete-reports',
] as const;

export async function preLaunchArgs(scenario: string, nonce: string): Promise<void> {
Expand Down Expand Up @@ -94,6 +96,8 @@ export async function runArgsScenario(scenario: string, nonce: string): Promise<
return;
case 'api-delete-false':
return deleteFalse(nonce);
case 'api-delete-reports':
return deleteReports(nonce);
}
}

Expand Down Expand Up @@ -297,3 +301,14 @@ async function deleteFalse(nonce: string): Promise<void> {
const deleted = await settle(() => Bugsee.deleteCollectedDataOnDevice(false));
mark(`delete done nonce=${nonce} stopped=${stopped} deleted=${deleted}`);
}

async function deleteReports(nonce: string): Promise<void> {
Bugsee.upload(`api-delete-reports-${nonce}`, '');
await delay(10_000);
mark(`delete-reports uploaded nonce=${nonce}`);
// The e2e lists the bundle waiting for upload (dead endpoint) in this window.
await delay(10_000);
const stopped = await settle(() => Bugsee.stop());
const deleted = await settle(() => Bugsee.deleteCollectedDataOnDevice(false, true));
mark(`delete-reports done nonce=${nonce} stopped=${stopped} deleted=${deleted}`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1816,10 +1816,12 @@ public String getAppearanceColor(final String name) {
@Override
public void deleteCollectedDataOnDevice(
final boolean includingIntermediate,
final boolean includingFinishedReports,
final Promise promise
) {
Bugsee.deleteCollectedDataOnDevice(
includingIntermediate,
includingFinishedReports,
success -> promise.resolve(Boolean.TRUE.equals(success))
);
}
Expand Down
65 changes: 29 additions & 36 deletions packages/react-native/ios/BugseeModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#import <BugseeRNSupport/BGSRNReactWindow.h>
#import <BugseeRNSupport/BGSRNExceptions.h>
#import <BugseeRNSupport/BGSRNSettleOnce.h>
#import <BugseeRNSupport/BGSRNDeleteCollectedData.h>
#import <BugseeRNSupport/BGSRNBlackout.h>
#import <BugseeRNSupport/BGSRNReportArgs.h>
#import <BugseeRNSupport/BGSRNCreatedReports.h>
Expand Down Expand Up @@ -66,6 +67,7 @@
#import "BGSRNReactWindow.h"
#import "BGSRNExceptions.h"
#import "BGSRNSettleOnce.h"
#import "BGSRNDeleteCollectedData.h"
#import "BGSRNBlackout.h"
#import "BGSRNReportArgs.h"
#import "BGSRNCreatedReports.h"
Expand Down Expand Up @@ -2700,47 +2702,38 @@ - (NSString *)getAppearanceColor:(NSString *)name {
return hex;
}

/// iOS 7.0.0-beta3 and beta4 return without invoking `completion` when the SDK is
/// not stopped, so a launched call resolves `false` immediately. That is the
/// missing completion, not a wait. Stopped, including a nil instance, still
/// calls the SDK method and settles through `BGSRNSettleOnce`: the
/// completion's success, or `false` if the deadline fires first. Status and
/// both SDK calls run on the main queue. A completion that arrives off main
/// hops back before it writes the result the settler resolves.
/// Resolves with the SDK's own outcome. iOS 7.0.0-beta7 always invokes the
/// completion (on the main thread): `NO` when the SDK is not stopped, otherwise
/// once the deletion has finished. The only timer is the long backstop in
/// `BGSRNDeleteCollectedData`, which REJECTS with `E_DELETE_TIMEOUT` (outcome
/// unknown, the deletion may still finish) rather than resolving `false`.
/// `includingFinishedReports` selects the SDK form that also deletes the report
/// bundles waiting for upload, the crashes not reported yet and the anomaly
/// baselines; that form has no `includingIntermediate` flag because it deletes
/// the snapshots too.
- (void)deleteCollectedDataOnDevice:(BOOL)includingIntermediate
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
includingFinishedReports:(BOOL)includingFinishedReports
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject {
BGSRNRunOnMain(^{
Bugsee *instance = [Bugsee sharedInstance];
BugseeStatus status = instance != nil ? instance.status : BugseeStatusStopped;
if (status != BugseeStatusStopped) {
__block BOOL settled = NO;
[Bugsee deleteCollectedDataOnDevice:includingIntermediate completion:^(BOOL success) {
BGSRNRunOnMain(^{
if (settled) {
return;
BGSRNDeleteCollectedData(
^(void (^completion)(BOOL)) {
if (includingFinishedReports) {
[Bugsee deleteCollectedDataOnDevice:completion];
} else {
[Bugsee deleteCollectedDataOnDevice:includingIntermediate completion:completion];
}
settled = YES;
},
BGSRNDeleteCollectedDataBackstopMs, dispatch_get_main_queue(),
^(BOOL success) {
resolve(@(success));
},
^(NSString *code) {
reject(code,
@"Bugsee did not report the outcome of deleteCollectedDataOnDevice in time; "
@"the deletion may still finish.",
nil);
});
}];
if (!settled) {
settled = YES;
resolve(@NO);
}
return;
}
__block NSNumber *result = @NO;
dispatch_block_t done =
BGSRNSettleOnce(BGSRNUnhandledCompletionDeadlineMs, dispatch_get_main_queue(), ^{
resolve(result);
});
[Bugsee deleteCollectedDataOnDevice:includingIntermediate completion:^(BOOL success) {
BGSRNRunOnMain(^{
result = @(success);
done();
});
}];
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#import "BGSRNDeleteCollectedData.h"

const int64_t BGSRNDeleteCollectedDataBackstopMs = 60000;

NSString *const BGSRNDeleteTimeoutCode = @"E_DELETE_TIMEOUT";

void BGSRNDeleteCollectedData(BGSRNDeleteSDKCall sdkCall,
int64_t backstopMs,
dispatch_queue_t queue,
void (^resolve)(BOOL success),
void (^reject)(NSString *code)) {
// Every touch of `settled` is on `queue`, which is serial, so a plain flag
// is enough: the completion and the backstop cannot interleave.
__block BOOL settled = NO;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, backstopMs * (int64_t)NSEC_PER_MSEC), queue, ^{
if (settled) {
return;
}
settled = YES;
reject(BGSRNDeleteTimeoutCode);
});

sdkCall(^(BOOL success) {
dispatch_async(queue, ^{
if (settled) {
return;
}
settled = YES;
resolve(success);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/// The longest `BGSRNDeleteCollectedData` waits for the SDK completion. Long on
/// purpose: a deletion that is merely slow (it can queue behind an upload that
/// holds the SDK's processing queue) must still report its real outcome, so
/// this is a backstop against a promise that never settles, not a deadline.
FOUNDATION_EXPORT const int64_t BGSRNDeleteCollectedDataBackstopMs;

/// Reject code when the backstop fires first. The deletion may still finish
/// later; the caller learns that the outcome is unknown, not that it failed.
FOUNDATION_EXPORT NSString *const BGSRNDeleteTimeoutCode;

/// The SDK call, handed the completion to invoke with the SDK's outcome.
typedef void (^BGSRNDeleteSDKCall)(void (^completion)(BOOL success));

/// Runs `sdkCall` and settles exactly once, on `queue` (which must be serial):
/// `resolve` with the SDK's success, or `reject` with
/// `BGSRNDeleteTimeoutCode` if `backstopMs` passes first. A completion that
/// arrives after the backstop, or a second completion, is ignored. The
/// completion may be called on any thread.
FOUNDATION_EXPORT void BGSRNDeleteCollectedData(BGSRNDeleteSDKCall sdkCall,
int64_t backstopMs,
dispatch_queue_t queue,
void (^resolve)(BOOL success),
void (^reject)(NSString *code));

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

/// Pins `BGSRNDeleteCollectedData`: the promise carries the SDK's outcome however
/// long it takes, and only a long backstop, as a rejection, ends a wait that
/// never completes.
@interface BGSRNDeleteCollectedDataTests : XCTestCase
@end

@implementation BGSRNDeleteCollectedDataTests

- (dispatch_queue_t)queue {
return dispatch_queue_create("com.bugsee.rn.delete.test", DISPATCH_QUEUE_SERIAL);
}

- (void)testResolvesWithTheSDKOutcome {
for (NSNumber *outcome in @[ @YES, @NO ]) {
XCTestExpectation *done = [self expectationWithDescription:@"resolved"];
__block NSInteger settles = 0;
__block BOOL value = !outcome.boolValue;
BGSRNDeleteCollectedData(
^(void (^completion)(BOOL)) {
completion(outcome.boolValue);
},
5000, [self queue],
^(BOOL success) {
settles += 1;
value = success;
[done fulfill];
},
^(NSString *code) {
XCTFail(@"unexpected reject %@", code);
});
[self waitForExpectations:@[ done ] timeout:2];
XCTAssertEqual(settles, 1);
XCTAssertEqual(value, outcome.boolValue);
}
}

- (void)testASlowCompletionPastTheOldOneAndAHalfSecondDeadlineStillResolvesItsOutcome {
XCTestExpectation *done = [self expectationWithDescription:@"resolved late"];
dispatch_queue_t queue = [self queue];
__block BOOL value = NO;
BGSRNDeleteCollectedData(
^(void (^completion)(BOOL)) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_MSEC),
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
completion(YES);
});
},
5000, queue,
^(BOOL success) {
value = success;
[done fulfill];
},
^(NSString *code) {
XCTFail(@"unexpected reject %@", code);
});
[self waitForExpectations:@[ done ] timeout:3];
XCTAssertTrue(value);
}

- (void)testRejectsWithATimeoutCodeWhenTheCompletionNeverComes {
XCTestExpectation *done = [self expectationWithDescription:@"rejected"];
__block NSString *rejectedCode = nil;
BGSRNDeleteCollectedData(
^(void (^completion)(BOOL)) {
},
50, [self queue],
^(BOOL success) {
XCTFail(@"must not resolve");
},
^(NSString *code) {
rejectedCode = code;
[done fulfill];
});
[self waitForExpectations:@[ done ] timeout:2];
XCTAssertEqualObjects(rejectedCode, @"E_DELETE_TIMEOUT");
XCTAssertEqualObjects(BGSRNDeleteTimeoutCode, @"E_DELETE_TIMEOUT");
XCTAssertEqual(BGSRNDeleteCollectedDataBackstopMs, 60000);
}

- (void)testALateCompletionAfterTheBackstopIsIgnored {
XCTestExpectation *rejected = [self expectationWithDescription:@"rejected"];
__block void (^lateCompletion)(BOOL) = nil;
__block NSInteger settles = 0;
dispatch_queue_t queue = [self queue];
BGSRNDeleteCollectedData(
^(void (^completion)(BOOL)) {
lateCompletion = [completion copy];
},
50, queue,
^(BOOL success) {
settles += 1;
},
^(NSString *code) {
settles += 1;
[rejected fulfill];
});
[self waitForExpectations:@[ rejected ] timeout:2];
lateCompletion(YES);
lateCompletion(NO);
XCTestExpectation *drained = [self expectationWithDescription:@"queue drained"];
dispatch_async(queue, ^{
[drained fulfill];
});
[self waitForExpectations:@[ drained ] timeout:2];
XCTAssertEqual(settles, 1);
}

- (void)testASecondCompletionIsIgnored {
XCTestExpectation *done = [self expectationWithDescription:@"resolved"];
__block NSInteger settles = 0;
dispatch_queue_t queue = [self queue];
BGSRNDeleteCollectedData(
^(void (^completion)(BOOL)) {
completion(YES);
completion(NO);
},
5000, queue,
^(BOOL success) {
settles += 1;
XCTAssertTrue(success);
},
^(NSString *code) {
XCTFail(@"unexpected reject");
});
dispatch_async(queue, ^{
[done fulfill];
});
[self waitForExpectations:@[ done ] timeout:2];
XCTAssertEqual(settles, 1);
}

@end
Loading
Loading