diff --git a/examples/bare/e2e/api-arguments.test.ts b/examples/bare/e2e/api-arguments.test.ts index 186c91cf..35879315 100644 --- a/examples/bare/e2e/api-arguments.test.ts +++ b/examples/bare/e2e/api-arguments.test.ts @@ -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(done.text, 'stopped')).toEqual({ ok: true, value: true }); + expect(jsonAfter(done.text, 'deleted')).toEqual({ ok: true, value: true }); + expect(bundlesBefore.length).toBe(1); + expect(bundlesAfter).toEqual([]); + expect(bundlesRelaunched).toEqual([]); + }); + it('[API-31] 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(); diff --git a/examples/bare/scenarios/api-args.ts b/examples/bare/scenarios/api-args.ts index 8c5945ce..a70678da 100644 --- a/examples/bare/scenarios/api-args.ts +++ b/examples/bare/scenarios/api-args.ts @@ -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). */ @@ -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 { @@ -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); } } @@ -297,3 +301,14 @@ async function deleteFalse(nonce: string): Promise { const deleted = await settle(() => Bugsee.deleteCollectedDataOnDevice(false)); mark(`delete done nonce=${nonce} stopped=${stopped} deleted=${deleted}`); } + +async function deleteReports(nonce: string): Promise { + 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}`); +} diff --git a/packages/react-native/android/src/main/java/com/bugsee/reactnative/BugseeModule.java b/packages/react-native/android/src/main/java/com/bugsee/reactnative/BugseeModule.java index a4bea468..617b066d 100644 --- a/packages/react-native/android/src/main/java/com/bugsee/reactnative/BugseeModule.java +++ b/packages/react-native/android/src/main/java/com/bugsee/reactnative/BugseeModule.java @@ -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)) ); } diff --git a/packages/react-native/ios/BugseeModule.mm b/packages/react-native/ios/BugseeModule.mm index 40de435f..3235a347 100644 --- a/packages/react-native/ios/BugseeModule.mm +++ b/packages/react-native/ios/BugseeModule.mm @@ -37,6 +37,7 @@ #import #import #import +#import #import #import #import @@ -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" @@ -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(); - }); - }]; }); } diff --git a/packages/react-native/ios/Support/Sources/BugseeRNSupport/BGSRNDeleteCollectedData.m b/packages/react-native/ios/Support/Sources/BugseeRNSupport/BGSRNDeleteCollectedData.m new file mode 100644 index 00000000..22877103 --- /dev/null +++ b/packages/react-native/ios/Support/Sources/BugseeRNSupport/BGSRNDeleteCollectedData.m @@ -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); + }); + }); +} diff --git a/packages/react-native/ios/Support/Sources/BugseeRNSupport/include/BGSRNDeleteCollectedData.h b/packages/react-native/ios/Support/Sources/BugseeRNSupport/include/BGSRNDeleteCollectedData.h new file mode 100644 index 00000000..cf8ea25d --- /dev/null +++ b/packages/react-native/ios/Support/Sources/BugseeRNSupport/include/BGSRNDeleteCollectedData.h @@ -0,0 +1,29 @@ +#import + +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 diff --git a/packages/react-native/ios/Support/Tests/BugseeRNSupportTests/BGSRNDeleteCollectedDataTests.m b/packages/react-native/ios/Support/Tests/BugseeRNSupportTests/BGSRNDeleteCollectedDataTests.m new file mode 100644 index 00000000..6388d422 --- /dev/null +++ b/packages/react-native/ios/Support/Tests/BugseeRNSupportTests/BGSRNDeleteCollectedDataTests.m @@ -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 diff --git a/packages/react-native/src/NativeBugsee.ts b/packages/react-native/src/NativeBugsee.ts index d03d122b..3f97ce23 100644 --- a/packages/react-native/src/NativeBugsee.ts +++ b/packages/react-native/src/NativeBugsee.ts @@ -607,14 +607,25 @@ export interface Spec extends TurboModule { getAppearanceColor(name: string): string; /** * Deletes collected data on the device. `includingIntermediate` is the - * SDK's own flag: Android `Bugsee.deleteCollectedDataOnDevice` and iOS - * `deleteCollectedDataOnDevice:completion:`. - * - * Resolves to the SDK's completion value. Both platforms refuse the - * deletion while launched; the promise still settles (`false`) so a - * caller is not left waiting on a completion the SDK does not invoke. - */ - deleteCollectedDataOnDevice(includingIntermediate: boolean): Promise; + * SDK's own flag. `includingFinishedReports` also deletes the finished + * reports waiting for upload: Android's 3-argument + * `Bugsee.deleteCollectedDataOnDevice(includingIntermediate, + * includingBundles, callback)`, which deletes the finished bundles only; + * iOS has no flags form for it, so `true` calls `deleteCollectedDataOnDevice:` + * (everything: also crashes not yet reported and the performance baselines; + * `includingIntermediate` is then moot). That gap is a possible SDK + * follow-up. `includingIntermediate` covers the report snapshots on both + * platforms and, on Android, exported reports. + * + * Resolves to the SDK's completion value, however long the deletion takes. + * Both platforms refuse the deletion while launched and answer `false` + * (by design). iOS rejects with `E_DELETE_TIMEOUT` if the SDK reports + * nothing for 60 s; the deletion may still finish. + */ + deleteCollectedDataOnDevice( + includingIntermediate: boolean, + includingFinishedReports: boolean, + ): Promise; } // Before the lookup: on the legacy architecture getEnforcing does not throw, diff --git a/packages/react-native/src/__mocks__/native.ts b/packages/react-native/src/__mocks__/native.ts index 2a106fd9..3e6f72bc 100644 --- a/packages/react-native/src/__mocks__/native.ts +++ b/packages/react-native/src/__mocks__/native.ts @@ -367,7 +367,7 @@ export const native = { setAppearanceColor: jest.fn(), getAppearanceColor: jest.fn(), - deleteCollectedDataOnDevice: jest.fn, [boolean]>(), + deleteCollectedDataOnDevice: jest.fn, [boolean, boolean]>(), /** * Resets every mock on this object, found rather than listed. diff --git a/packages/react-native/src/appearance/__tests__/report.test.ts b/packages/react-native/src/appearance/__tests__/report.test.ts index 06784f37..6f3001b3 100644 --- a/packages/react-native/src/appearance/__tests__/report.test.ts +++ b/packages/react-native/src/appearance/__tests__/report.test.ts @@ -164,48 +164,56 @@ describe('report appearance mapping', () => { }); describe('deleteCollectedDataOnDevice', () => { - it('passes includingIntermediate true through', async () => { + it('passes includingIntermediate true through, keeping finished reports by default', async () => { native.deleteCollectedDataOnDevice.mockResolvedValue(true); await expect(Bugsee.deleteCollectedDataOnDevice(true)).resolves.toBe(true); - expect(native.deleteCollectedDataOnDevice).toHaveBeenCalledWith(true); + expect(native.deleteCollectedDataOnDevice).toHaveBeenCalledWith(true, false); }); it('passes includingIntermediate false through', async () => { native.deleteCollectedDataOnDevice.mockResolvedValue(false); await expect(Bugsee.deleteCollectedDataOnDevice(false)).resolves.toBe(false); - expect(native.deleteCollectedDataOnDevice).toHaveBeenCalledWith(false); + expect(native.deleteCollectedDataOnDevice).toHaveBeenCalledWith(false, false); }); - it('refuses immediately while launched and settles the Stopped path', () => { + it('passes includingFinishedReports through so finished reports can be deleted', async () => { + native.deleteCollectedDataOnDevice.mockResolvedValue(true); + await expect(Bugsee.deleteCollectedDataOnDevice(false, true)).resolves.toBe(true); + expect(native.deleteCollectedDataOnDevice).toHaveBeenCalledWith(false, true); + }); + + describe('iOS bridge', () => { const source = readFileSync(join(__dirname, '../../../ios/BugseeModule.mm'), 'utf8'); const start = source.indexOf('- (void)deleteCollectedDataOnDevice:'); const end = source.indexOf('- (std::shared_ptr)getTurboModule:', start); const method = source.slice(start, end); - const hop = method.indexOf('BGSRNRunOnMain('); - const status = method.indexOf('instance.status'); - const nilStopped = method.indexOf('BugseeStatusStopped'); - const launched = method.indexOf('status != BugseeStatusStopped'); - const call = method.indexOf('[Bugsee deleteCollectedDataOnDevice:includingIntermediate'); - const earlyResolve = method.indexOf('resolve(@NO)'); - const launchedReturn = method.indexOf('\n return;', earlyResolve); - const settle = method.indexOf('BGSRNSettleOnce(BGSRNUnhandledCompletionDeadlineMs'); - const stoppedCall = method.indexOf( - '[Bugsee deleteCollectedDataOnDevice:includingIntermediate', - settle, + + // The settle-once/backstop behaviour is tested for real in + // ios/Support/Tests/.../BGSRNDeleteCollectedDataTests.m; this only pins the + // wiring to the SDK forms. + it('delegates settling to BGSRNDeleteCollectedData, with no resolve-false timeout', () => { + expect(start).toBeGreaterThan(-1); + expect(method).toContain('BGSRNDeleteCollectedData('); + expect(method).toContain('BGSRNDeleteCollectedDataBackstopMs'); + expect(method).toContain('resolve(@(success))'); + expect(method).not.toContain('BGSRNSettleOnce'); + expect(method).not.toContain('resolve(@NO)'); + }); + + it('calls the delete-everything form only when finished reports are included', () => { + expect(method).toMatch(/if \(includingFinishedReports\) \{\s*\[Bugsee deleteCollectedDataOnDevice:completion\];/); + expect(method).toMatch(/\} else \{\s*\[Bugsee deleteCollectedDataOnDevice:includingIntermediate completion:completion\];/); + }); + }); + + it('Android bridge forwards includingFinishedReports to the 3-argument SDK method', () => { + const java = readFileSync( + join(__dirname, '../../../android/src/main/java/com/bugsee/reactnative/BugseeModule.java'), + 'utf8', ); - expect(hop).toBeGreaterThan(-1); - expect(method).not.toContain('BGSRNRunOnMainSync'); - expect(status).toBeGreaterThan(hop); - expect(nilStopped).toBeGreaterThan(status); - expect(launched).toBeGreaterThan(nilStopped); - expect(call).toBeGreaterThan(launched); - expect(earlyResolve).toBeGreaterThan(call); - expect(launchedReturn).toBeGreaterThan(earlyResolve); - expect(settle).toBeGreaterThan(launchedReturn); - expect(method.slice(0, settle)).not.toContain('BGSRNSettleOnce'); - expect(method.slice(0, settle)).not.toContain('BGSRNUnhandledCompletionDeadlineMs'); - expect(stoppedCall).toBeGreaterThan(settle); - expect(method.slice(settle)).toContain('@(success)'); - expect(method).toContain('includingIntermediate'); + const at = java.indexOf('public void deleteCollectedDataOnDevice('); + const body = java.slice(at, java.indexOf('}\n', java.indexOf('promise.resolve', at))); + expect(body).toContain('final boolean includingFinishedReports'); + expect(body).toMatch(/Bugsee\.deleteCollectedDataOnDevice\(\s*includingIntermediate,\s*includingFinishedReports,/); }); }); diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index c05bf942..094b868e 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -835,17 +835,36 @@ class Bugsee { } /** - * Deletes collected data stored on the device. - * - * `includingIntermediate` crosses unchanged. Both SDKs delete the rolling - * capture data whatever it is, and keep finished bundles: - * - `false`: deletes the capture data, keeps finished bundles. - * - `true`: also deletes intermediate data (snapshots, exported reports). - * The SDK must be stopped for the deletion to succeed. While launched, both - * platforms refuse it and this resolves `false`. - */ - deleteCollectedDataOnDevice(includingIntermediate: boolean): Promise { - return NativeBugsee.deleteCollectedDataOnDevice(includingIntermediate); + * Deletes collected data stored on the device. The SDK must be stopped + * first: while launched, both platforms refuse (by design) and this + * resolves `false`. The promise resolves with the SDK's own outcome, however + * long the deletion takes. On iOS, if the SDK reports nothing for 60 s (with either + * flag value), it rejects with code `E_DELETE_TIMEOUT`: + * the outcome is unknown and the deletion may still finish. + * + * - `includingIntermediate`: `false` deletes the rolling capture data and + * keeps finished reports; `true` also deletes the data set aside for + * reports not yet bundled (report snapshots on both platforms; on Android + * also exported reports). + * - `includingFinishedReports` (default `false`): also deletes the finished + * reports waiting for upload, which are otherwise still uploaded after + * the data is deleted (use it when a user withdraws consent). + * + * Platform difference with `includingFinishedReports: true`: iOS calls the + * SDK's delete-everything form, which also deletes crashes not yet reported + * and the learned performance baselines, and cancels pending uploads + * (`includingIntermediate` is then moot). Android's 3-argument SDK method + * deletes the finished report bundles only; it does not touch crashes not + * yet reported. TODO: possible SDK follow-up to align the two. + */ + deleteCollectedDataOnDevice( + includingIntermediate: boolean, + includingFinishedReports: boolean = false, + ): Promise { + return NativeBugsee.deleteCollectedDataOnDevice( + includingIntermediate, + includingFinishedReports, + ); } }