Repository: getsentry/sentry-capacitor (ts)

Issue:
getCurrentHub: Incorrect deprecation notice?
Hey all!

I'm currently trying to close the Sentry connection on runtime whenever we detect an app update is available and the user has been prompted. As I'm lead to believe the current way to do so is as below, however, this prompts a deprecation notice to use `Sentry.getClient()` instead. This method does not exist, thus it seems this deprecation notice is incorrect.

```js
Sentry.getCurrentHub().getClient().close(0); // deprecated, but works
Sentry.getClient().close(0); // Sentry.getClient is not a function
```

 In contrast it does seem to work correctly when trying to close a Vue-specific instance of Sentry. In other words, this following prompts (correctly) to use the alternative:

```js
SentryVue.getCurrentHub().getClient().close(0); // deprecated, but works
SentryVue.getClient().close(0); // works just fine
```

While having to go through this many hoops to simply disable Sentry seems like a drag, that is a separate issue, though I would like to know whether this is correct, or whether only `Sentry` or `SentryVue` needs to be closed.

I'm currently using the following Sentry-relevant packages:

```json
"@sentry/capacitor": "0.18.0",
"@sentry/vite-plugin": "2.18.0",
"@sentry/vue": "7.114.0",
```

Pull request description:
Feat: Add close function to the main SDK.
This PR exposes the close function that closes the client and the native layer.

Minor changes:

# Sample
* Removed deprecated flags from the latest samples.
* Added a button to call the close function.
Closes #663

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd91cd1..a3ee92e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,11 @@
 
 ### Features
 
+- Add close function on `@sentry/capacitor` ([#664](https://github.com/getsentry/sentry-capacitor/pull/664))
+- Expose `getClient` on `@sentry/capacitor` ([#664](https://github.com/getsentry/sentry-capacitor/pull/664))
 - Add the following functions to `@sentry/capacitor` ([#666](https://github.com/getsentry/sentry-capacitor/pull/666))
   - `flush`, `getActiveSpan`, `spanToJSON`, `spanIsSampled`, `setMeasurement`, `getGlobalScope` and `setCurrentClient`.
 
-
 ### Fixes
 
 - Accept undefined as value for tags ([#656](https://github.com/getsentry/sentry-capacitor/pull/656))
diff --git a/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java b/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java
index 08b22c7..649d415 100644
--- a/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java
+++ b/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java
@@ -333,6 +333,12 @@ public class SentryCapacitor extends Plugin {
         });
     }
 
+    @PluginMethod
+    public void closeNativeSdk(PluginCall call) {
+        Sentry.close();
+        call.resolve();
+    }
+
     @PluginMethod
     public void setExtra(PluginCall call) {
         if (call.getData().has("key") && call.getData().has("value")) {
diff --git a/example/ionic-angular-v3/src/app/tab2/tab2.page.html b/example/ionic-angular-v3/src/app/tab2/tab2.page.html
index 23cd8e5..ad3b455 100644
--- a/example/ionic-angular-v3/src/app/tab2/tab2.page.html
+++ b/example/ionic-angular-v3/src/app/tab2/tab2.page.html
@@ -39,5 +39,12 @@
         >Capture context message</ion-button
       >
     </label>
+
+    <label>
+      Close the SDK
+      <ion-button (click)="close()"
+        >Close the SDK</ion-button
+      >
+    </label>
   </section>
 </ion-content>
diff --git a/example/ionic-angular-v3/src/app/tab2/tab2.page.ts b/example/ionic-angular-v3/src/app/tab2/tab2.page.ts
index 9df0192..3e8e059 100644
--- a/example/ionic-angular-v3/src/app/tab2/tab2.page.ts
+++ b/example/ionic-angular-v3/src/app/tab2/tab2.page.ts
@@ -38,4 +38,8 @@ export class Tab2Page {
       `${Date.now()}: Captured message with added context.`,
     );
   }
+
+  public close(): void {
+    Sentry.close();
+  }
 }
diff --git a/example/ionic-angular-v4/src/app/tab2/tab2.page.html b/example/ionic-angular-v4/src/app/tab2/tab2.page.html
index 23cd8e5..ad3b455 100644
--- a/example/ionic-angular-v4/src/app/tab2/tab2.page.html
+++ b/example/ionic-angular-v4/src/app/tab2/tab2.page.html
@@ -39,5 +39,12 @@
         >Capture context message</ion-button
       >
     </label>
+
+    <label>
+      Close the SDK
+      <ion-button (click)="close()"
+        >Close the SDK</ion-button
+      >
+    </label>
   </section>
 </ion-content>
diff --git a/example/ionic-angular-v4/src/app/tab2/tab2.page.ts b/example/ionic-angular-v4/src/app/tab2/tab2.page.ts
index 2250902..ad7ab51 100644
--- a/example/ionic-angular-v4/src/app/tab2/tab2.page.ts
+++ b/example/ionic-angular-v4/src/app/tab2/tab2.page.ts
@@ -37,4 +37,8 @@ export class Tab2Page {
       `${Date.now()}: Captured message with added context.`,
     );
   }
+  public close(): void {
+    Sentry.close();
+  }
+
 }
diff --git a/example/ionic-angular-v5/src/app/tab2/tab2.page.html b/example/ionic-angular-v5/src/app/tab2/tab2.page.html
index 23cd8e5..ad3b455 100644
--- a/example/ionic-angular-v5/src/app/tab2/tab2.page.html
+++ b/example/ionic-angular-v5/src/app/tab2/tab2.page.html
@@ -39,5 +39,12 @@
         >Capture context message</ion-button
       >
     </label>
+
+    <label>
+      Close the SDK
+      <ion-button (click)="close()"
+        >Close the SDK</ion-button
+      >
+    </label>
   </section>
 </ion-content>
diff --git a/example/ionic-angular-v5/src/app/tab2/tab2.page.ts b/example/ionic-angular-v5/src/app/tab2/tab2.page.ts
index 2250902..6bd6142 100644
--- a/example/ionic-angular-v5/src/app/tab2/tab2.page.ts
+++ b/example/ionic-angular-v5/src/app/tab2/tab2.page.ts
@@ -37,4 +37,8 @@ export class Tab2Page {
       `${Date.now()}: Captured message with added context.`,
     );
   }
+
+  public close(): void {
+    Sentry.close();
+  }
 }
diff --git a/example/ionic-angular-v6/android/app/src/main/assets/capacitor.config.json b/example/ionic-angular-v6/android/app/src/main/assets/capacitor.config.json
index 7c1283c..08df513 100644
--- a/example/ionic-angular-v6/android/app/src/main/assets/capacitor.config.json
+++ b/example/ionic-angular-v6/android/app/src/main/assets/capacitor.config.json
@@ -1,7 +1,6 @@
 {
 	"appId": "io.ionic.starter",
 	"appName": "ionic-angular",
-	"bundledWebRuntime": false,
 	"plugins": {
 		"SplashScreen": {
 			"launchShowDuration": 0
diff --git a/example/ionic-angular-v6/capacitor.config.ts b/example/ionic-angular-v6/capacitor.config.ts
index ad1faed..7e75e9b 100644
--- a/example/ionic-angular-v6/capacitor.config.ts
+++ b/example/ionic-angular-v6/capacitor.config.ts
@@ -3,7 +3,6 @@ import { CapacitorConfig } from '@capacitor/cli';
 const config: CapacitorConfig = {
   appId: 'io.ionic.starter',
   appName: 'ionic-angular',
-  bundledWebRuntime: false,
   plugins: {
     SplashScreen: {
       launchShowDuration: 0,
diff --git a/example/ionic-angular-v6/ios/App/App/capacitor.config.json b/example/ionic-angular-v6/ios/App/App/capacitor.config.json
index 82e693d..8d86eaf 100644
--- a/example/ionic-angular-v6/ios/App/App/capacitor.config.json
+++ b/example/ionic-angular-v6/ios/App/App/capacitor.config.json
@@ -1,7 +1,6 @@
 {
 	"appId": "io.ionic.starter",
 	"appName": "ionic-angular",
-	"bundledWebRuntime": false,
 	"plugins": {
 		"SplashScreen": {
 			"launchShowDuration": 0
diff --git a/example/ionic-angular-v6/src/app/tab2/tab2.page.html b/example/ionic-angular-v6/src/app/tab2/tab2.page.html
index 23cd8e5..ad3b455 100644
--- a/example/ionic-angular-v6/src/app/tab2/tab2.page.html
+++ b/example/ionic-angular-v6/src/app/tab2/tab2.page.html
@@ -39,5 +39,12 @@
         >Capture context message</ion-button
       >
     </label>
+
+    <label>
+      Close the SDK
+      <ion-button (click)="close()"
+        >Close the SDK</ion-button
+      >
+    </label>
   </section>
 </ion-content>
diff --git a/example/ionic-angular-v6/src/app/tab2/tab2.page.ts b/example/ionic-angular-v6/src/app/tab2/tab2.page.ts
index 2250902..6bd6142 100644
--- a/example/ionic-angular-v6/src/app/tab2/tab2.page.ts
+++ b/example/ionic-angular-v6/src/app/tab2/tab2.page.ts
@@ -37,4 +37,8 @@ export class Tab2Page {
       `${Date.now()}: Captured message with added context.`,
     );
   }
+
+  public close(): void {
+    Sentry.close();
+  }
 }
diff --git a/ios/Plugin/Plugin.m b/ios/Plugin/Plugin.m
index b362d20..8e0e481 100644
--- a/ios/Plugin/Plugin.m
+++ b/ios/Plugin/Plugin.m
@@ -15,5 +15,6 @@ CAP_PLUGIN(SentryCapacitor, "SentryCapacitor",
            CAP_PLUGIN_METHOD(setUser, CAPPluginReturnPromise);
            CAP_PLUGIN_METHOD(addBreadcrumb, CAPPluginReturnPromise);
            CAP_PLUGIN_METHOD(clearBreadcrumbs, CAPPluginReturnPromise);
+           CAP_PLUGIN_METHOD(closeNativeSdk, CAPPluginReturnPromise);
            CAP_PLUGIN_METHOD(setContext, CAPPluginReturnPromise);
            CAP_PLUGIN_METHOD(crash, CAPPluginReturnPromise);)
diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift
index ca1fc40..914a975 100644
--- a/ios/Plugin/Plugin.swift
+++ b/ios/Plugin/Plugin.swift
@@ -281,6 +281,11 @@ public class SentryCapacitor: CAPPlugin {
         call.resolve()
     }
 
+    @objc func closeNativeSdk(_ call: CAPPluginCall ) {
+        SentrySDK.close()
+        call.resolve()
+    }
+
     @objc func crash(_ call: CAPPluginCall) {
         SentrySDK.crash()
     }
diff --git a/src/definitions.ts b/src/definitions.ts
index 0d15de4..7ed09b4 100644
--- a/src/definitions.ts
+++ b/src/definitions.ts
@@ -17,6 +17,7 @@ export interface ISentryCapacitorPlugin {
   ): PromiseLike<boolean>;
 
   clearBreadcrumbs(): void;
+  closeNativeSdk(): Promise<void>;
   crash(): void;
   fetchNativeRelease(): Promise<{
     build: string;
diff --git a/src/index.ts b/src/index.ts
index 717b7e0..d4f9454 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -28,6 +28,7 @@ export {
   getCurrentHub,
   Hub,
   Scope,
+  getClient,
   setContext,
   setExtra,
   setExtras,
@@ -56,4 +57,4 @@ export { Replay, BrowserTracing } from '@sentry/browser'
 
 export { SDK_NAME, SDK_VERSION } from './version';
 export type { CapacitorOptions } from './options';
-export { init, nativeCrash } from './sdk';
+export { init, nativeCrash, close } from './sdk';
diff --git a/src/sdk.ts b/src/sdk.ts
index 2da0b40..6572696 100644
--- a/src/sdk.ts
+++ b/src/sdk.ts
@@ -3,7 +3,8 @@ import {
   defaultIntegrations,
   init as browserInit
 } from '@sentry/browser';
-import { Hub, makeMain } from '@sentry/core';
+import { getClient, Hub, makeMain } from '@sentry/core';
+import { logger } from '@sentry/utils';
 
 import { DeviceContext, EventOrigin, Release, SdkInfo } from './integrations';
 import { createCapacitorRewriteFrames } from './integrations/rewriteframes';
@@ -83,6 +84,23 @@ export function init<T>(
   originalInit(browserOptions);
 }
 
+/**
+ * Closes the SDK, stops sending events.
+ */
+export async function close(): Promise<void> {
+  try {
+    const client = getClient();
+
+    if (client) {
+      await client.close();
+      await NATIVE.closeNativeSdk();
+    }
+  } catch (e) {
+    logger.error('Failed to close the SDK');
+  }
+}
+
+
 /**
  * If native client is available it will trigger a native crash
  * Use this only for testing purposes
diff --git a/src/wrapper.ts b/src/wrapper.ts
index 3225eaf..f63b0e8 100644
--- a/src/wrapper.ts
+++ b/src/wrapper.ts
@@ -275,6 +275,26 @@ export const NATIVE = {
     SentryCapacitor.clearBreadcrumbs();
   },
 
+
+  /**
+   * Closes the Native Layer SDK
+   */
+  closeNativeSdk(): Promise<void> {
+    if (!this.enableNative) {
+      // Only log and avoid rejecting.
+      logger.debug(this._DisabledNativeError);
+      return Promise.resolve();
+    }
+    if (!this.isNativeClientAvailable()) {
+      throw this._NativeClientError;
+    }
+
+    return SentryCapacitor.closeNativeSdk().then(() => {
+      this.enableNative = false;
+    });
+  },
+
+
   /**
    * Sets context on the native scope. Not implemented in Android yet.
    * @param key string


Test patch:
diff --git a/test/wrapper.test.ts b/test/wrapper.test.ts
index 26c1fca..3c5a0ba 100644
--- a/test/wrapper.test.ts
+++ b/test/wrapper.test.ts
@@ -61,6 +61,7 @@ jest.mock('../src/plugin', () => {
       setUser: jest.fn(() => {
         return;
       }),
+      closeNativeSdk: jest.fn(() => Promise.resolve()),
     },
   };
 });
@@ -625,4 +626,22 @@ describe('Tests Native Wrapper', () => {
       expect(SentryCapacitor.fetchNativeDeviceContexts).not.toBeCalled();
     });
   });
+
+  describe('closeNativeSdk', () => {
+    test('closeNativeSdk calls native bridge', async () => {
+      await NATIVE.closeNativeSdk();
+
+      expect(SentryCapacitor.closeNativeSdk).toBeCalled();
+      expect(NATIVE.enableNative).toBe(false);
+    });
+  });
+
+  test('closeNativeSdk called twice does not throw error.', async () => {
+    logger.debug = jest.fn();
+    await NATIVE.closeNativeSdk();
+    expect(logger.debug).not.toBeCalled();
+
+    await NATIVE.closeNativeSdk();
+    expect(logger.debug).toBeCalledWith(NATIVE._DisabledNativeError);
+  });
 });