Repository: getsentry/sentry-capacitor
Pull request #10 (2020-11-18 18:22:53)

fix: updated type of data received to fix captureenvelope and getstringbyteslength android methods
This implements the captureMessage and captureException functionality in android and in the JS middleware. There are also fixes to the tests and updates to the type of data being sent to captureEnvelope and getStringBytesLength (the wrong data was previously being sent).

Testing of both the captureException and captureMessage methods was used in a project I created on the Sentry website, and both events were successfully sent: 
![Screen Shot 2020-11-18 at 1 22 44 PM](https://user-images.githubusercontent.com/42003899/99571345-28728780-29a1-11eb-8dd3-0f22eb8e3ee5.png)



Patch:
diff --git a/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java b/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java
index 0e36852..ab28c1c 100644
--- a/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java
+++ b/android/src/main/java/io/sentry/capacitor/SentryCapacitor.java
@@ -1,40 +1,59 @@
 package io.sentry.capacitor;
 
+import com.getcapacitor.JSArray;
+import com.getcapacitor.JSObject;
 import com.getcapacitor.NativePlugin;
 import com.getcapacitor.Plugin;
 import com.getcapacitor.PluginCall;
 import com.getcapacitor.PluginMethod;
 
+import io.sentry.Breadcrumb;
 import io.sentry.Integration;
+import io.sentry.Sentry;
+import io.sentry.SentryLevel;
 import io.sentry.SentryOptions;
 import io.sentry.UncaughtExceptionHandlerIntegration;
 import io.sentry.android.core.AnrIntegration;
 import io.sentry.android.core.NdkIntegration;
 import io.sentry.android.core.SentryAndroid;
 import io.sentry.protocol.SdkVersion;
-import io.sentry.protocol.SentryException;
 
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
 import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.UUID;
 
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.util.Log;
+
 @NativePlugin
 public class SentryCapacitor extends Plugin {
 
-    static final Logger logger = Logger.getLogger("capacitor-sentry");
+    final static Logger logger = Logger.getLogger("capacitor-sentry");
     private SentryOptions sentryOptions;
+    private Context context;
+    private static PackageInfo packageInfo;
 
-    // @PluginMethod
-    // public void echo(PluginCall call) {
-    //     String value = call.getString("value");
+    @Override
+    public void load() {
+        super.load();
 
-    //     JSObject ret = new JSObject();
-    //     ret.put("value", value);
-    //     call.success(ret);
-    // }
+        if (this.context == null) {
+            this.context = this.bridge.getContext();
+        }
+
+        try {
+            this.packageInfo = this.context.getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
+        } catch (Exception e) {
+            logger.info("Error getting package info.");
+        }
+    }
 
     @PluginMethod
     public void startWithOptions(final PluginCall capOptions) {
@@ -77,19 +96,6 @@ public class SentryCapacitor extends Plugin {
 
                 options.setBeforeSend(
                     (event, hint) -> {
-                        // TODO Need to look into whether this is needed for Capacitor
-                        // React native internally throws a JavascriptException
-                        // Since we catch it before that, we don't want to send this one
-                        // because we would send it twice
-                        try {
-                            SentryException ex = event.getExceptions().get(0);
-                            if (null != ex && ex.getType().contains("JavascriptException")) {
-                                return null;
-                            }
-                        } catch (Exception e) {
-                            // We do nothing
-                        }
-
                         // Add on the correct event.origin tag.
                         // it needs to be here so we can determine where it originated from.
                         SdkVersion sdkVersion = event.getSdk();
@@ -132,7 +138,37 @@ public class SentryCapacitor extends Plugin {
             }
         );
 
-        capOptions.resolve();
+        JSObject resp = new JSObject();
+        resp.put("value", true);
+        capOptions.resolve(resp);
+    }
+
+    @PluginMethod
+    public void setLogLevel(PluginCall call) {
+        int level = call.getInt("level", 2);
+        logger.setLevel(this.logLevel(level));
+    }
+
+    private Level logLevel(int level) {
+        switch (level) {
+            case 1:
+                return Level.SEVERE;
+            case 2:
+                return Level.INFO;
+            case 3:
+                return Level.ALL;
+            default:
+                return Level.OFF;
+        }
+    }
+
+    @PluginMethod
+    public void fetchRelease(PluginCall call) {
+        JSObject release = new JSObject();
+        release.put("id", this.packageInfo.packageName);
+        release.put("version", this.packageInfo.versionName);
+        release.put("build", String.valueOf(this.packageInfo.versionCode));
+        call.resolve(release);
     }
 
     @PluginMethod
@@ -142,15 +178,119 @@ public class SentryCapacitor extends Plugin {
             File installation =  new File(sentryOptions.getOutboxPath(), UUID.randomUUID().toString());
             try (FileOutputStream out = new FileOutputStream(installation)) {
                 out.write(envelope.getBytes(Charset.forName("UTF-8")));
-                logger.info("Successfully captured envelope");
+                logger.info("Successfully captured envelope.");
+
+                JSObject resp = new JSObject();
+                resp.put("value", envelope);
+                call.resolve(resp);
+
             } catch (Exception e) {
-                logger.info("Error writing envelope: ", e);
+                logger.info("Error writing envelope.");
+                call.reject(String.valueOf(e));
+                return;
             }
         }
         catch (Exception e) {
-            logger.info("Error reading envelope: ", e);
+            logger.info("Error reading envelope.");
+            call.reject(String.valueOf(e));
+            return;
+        }
+    }
+    
+    @PluginMethod
+    public void getStringBytesLength(PluginCall call) {
+        if (call.getData().has("string")) {
+            String payload = call.getString("string");
+            try {
+                JSObject resp = new JSObject();
+                resp.put("value", payload.getBytes("UTF-8").length);
+                call.resolve(resp);
+            } catch (UnsupportedEncodingException e) {
+                call.reject(String.valueOf(e));
+            }
+        } else {
+            call.reject("Could not calculate string length.");
         }
+    }
 
+    @PluginMethod
+    public void addBreadcrumb(final PluginCall breadcrumb) {
+        Sentry.configureScope(scope -> {
+            Breadcrumb breadcrumbInstance = new Breadcrumb();
+
+            if (breadcrumb.getData().has("message")) {
+                breadcrumbInstance.setMessage(breadcrumb.getString("message"));
+            }
+
+            if (breadcrumb.getData().has("type")) {
+                breadcrumbInstance.setType(breadcrumb.getString("type"));
+            }
+
+            if (breadcrumb.getData().has("category")) {
+                breadcrumbInstance.setCategory(breadcrumb.getString("category"));
+            }
+
+            if (breadcrumb.getData().has("level")) {
+                switch (breadcrumb.getString("level")) {
+                    case "fatal":
+                        breadcrumbInstance.setLevel(SentryLevel.FATAL);
+                        break;
+                    case "warning":
+                        breadcrumbInstance.setLevel(SentryLevel.WARNING);
+                        break;
+                    case "info":
+                        breadcrumbInstance.setLevel(SentryLevel.INFO);
+                        break;
+                    case "debug":
+                        breadcrumbInstance.setLevel(SentryLevel.DEBUG);
+                        break;
+                    case "error":
+                        breadcrumbInstance.setLevel(SentryLevel.ERROR);
+                        break;
+                    default:
+                        breadcrumbInstance.setLevel(SentryLevel.ERROR);
+                        break;
+                }
+            }
+
+            if (breadcrumb.getData().has("data")) {
+                Map<String, String> data = (Map<String, String>) breadcrumb.getArray("data");
+
+                for (Map.Entry<String, String> entry: data.entrySet()) {
+                    String key = entry.getKey();
+                    String value = entry.getValue();
+                    breadcrumbInstance.setData(key, value);
+                }
+            }
+
+            scope.addBreadcrumb(breadcrumbInstance);
+        });
+    }
+
+    @PluginMethod
+    public void clearBreadcrumbs() {
+        Sentry.configureScope(scope -> {
+            scope.clearBreadcrumbs();
+        });
+    }
+
+    @PluginMethod
+    public void setExtra(PluginCall call) {
+        String key = call.getString("key");
+        String extra = call.getString("extra");
+        Sentry.configureScope(scope -> {
+            scope.setExtra(key, extra);
+        });
+        call.resolve();
+    }
+
+    @PluginMethod
+    public void setTag(PluginCall call) {
+        String key = call.getString("key");
+        String value = call.getString("value");
+        Sentry.configureScope(scope -> {
+            scope.setTag(key, value);
+        });
         call.resolve();
     }
 }
diff --git a/src/backend.ts b/src/backend.ts
index 131ac94..0cf0e08 100644
--- a/src/backend.ts
+++ b/src/backend.ts
@@ -1,6 +1,7 @@
 import { BrowserOptions, Transports } from '@sentry/browser';
 import { BrowserBackend } from '@sentry/browser/dist/backend';
 import { BaseBackend, NoopTransport } from '@sentry/core';
+import { Event, EventHint, Severity, Transport } from '@sentry/types';
 import { logger } from '@sentry/utils';
 
 import { CapacitorOptions } from './options';
@@ -37,7 +38,29 @@ export class CapacitorBackend extends BaseBackend<BrowserOptions> {
   /**
    * @inheritDoc
    */
-  protected _setupCapacitorTransport(): Transport | NoopTransport {
+  public eventFromException(
+    exception: unknown,
+    hint?: EventHint,
+  ): PromiseLike<Event> {
+    return this._browserBackend.eventFromException(exception, hint);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  public eventFromMessage(
+    message: string,
+    level: Severity = Severity.Info,
+    hint?: EventHint,
+  ): PromiseLike<Event> {
+    /* eslint-disable no-console */
+    return this._browserBackend.eventFromMessage(message, level, hint);
+  }
+
+  /**
+   * @inheritDoc
+   */
+  protected _setupTransport(): Transport | NoopTransport {
     if (!this._options.dsn) {
       // We return the noop transport here in case there is no Dsn.
       return new NoopTransport();
@@ -59,16 +82,6 @@ export class CapacitorBackend extends BaseBackend<BrowserOptions> {
     return new Transports.FetchTransport(transportOptions);
   }
 
-  /**
-   * Has Capacitor on window?
-   */
-  // private _isCapacitor(): boolean {
-  //   return (
-  //     getGlobalObject<any>().capacitor !== undefined ||
-  //     getGlobalObject<any>().Capacitor !== undefined
-  //   );
-  // }
-
   /**
    * If true, native client is availabe and active
    */
diff --git a/src/definitions.ts b/src/definitions.ts
index 612b0b8..855e823 100644
--- a/src/definitions.ts
+++ b/src/definitions.ts
@@ -14,7 +14,7 @@ interface serializedObject {
 
 export interface SentryCapacitorPlugin {
   addBreadcrumb(breadcrumb: Breadcrumb): void;
-  captureEnvelope(envelope: string): PromiseLike<Response>;
+  captureEnvelope(payload: { envelope: string }): PromiseLike<Response>;
   clearBreadcrumbs(): void;
   crash(): void;
   fetchRelease(): Promise<{
@@ -22,7 +22,7 @@ export interface SentryCapacitorPlugin {
     id: string;
     version: string;
   }>;
-  getStringBytesLength(payloadString: string): number;
+  getStringBytesLength(payload: { string: string }): Promise<{ value: number }>;
   startWithOptions(options: CapacitorOptions): Promise<boolean>;
   setUser(
     user: serializedObject | null,
diff --git a/src/web.ts b/src/web.ts
index e96c78c..ae7c00b 100644
--- a/src/web.ts
+++ b/src/web.ts
@@ -27,8 +27,10 @@ export class SentryCapacitorWeb
   /**
    *
    */
-  captureEnvelope(): Promise<Response> {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  captureEnvelope(payload: { envelope: string }): Promise<Response> {
     // TODO integrate web
+
     return Promise.resolve({
       status: Status.Success,
     });
@@ -67,20 +69,20 @@ export class SentryCapacitorWeb
   /**
    *
    */
-  getStringBytesLength(payloadString: 'somepayload'): number {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  getStringBytesLength(payload: {
+    string: string;
+  }): Promise<{ value: number }> {
     // TODO integrate web
-    /* eslint-disable-next-line no-console */
-    console.log(payloadString);
-    return 12;
+    return Promise.resolve({ value: 12 });
   }
 
   /**
    *
    */
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
   async startWithOptions(options: CapacitorOptions): Promise<boolean> {
     // TODO integrate web
-    /* eslint-disable-next-line no-console */
-    console.log(options);
     return true;
   }
 
diff --git a/src/wrapper.ts b/src/wrapper.ts
index 2f04d8f..8d1d2c7 100644
--- a/src/wrapper.ts
+++ b/src/wrapper.ts
@@ -39,12 +39,15 @@ export const NATIVE = {
       },
     };
 
-    const headerString = JSON.stringify(header);
-
-    const payloadString = JSON.stringify(payload);
+    const headerString: string = JSON.stringify(header);
+    const payloadString: string = JSON.stringify(payload);
     let length = payloadString.length;
     try {
-      length = await SentryCapacitor.getStringBytesLength(payloadString);
+      void SentryCapacitor.getStringBytesLength({ string: payloadString }).then(
+        resp => {
+          length = resp.value;
+        },
+      );
     } catch {
       // The native call failed, we do nothing, we have payload.length as a fallback
     }
@@ -58,7 +61,7 @@ export const NATIVE = {
     const itemString = JSON.stringify(item);
 
     const envelopeString = `${headerString}\n${itemString}\n${payloadString}`;
-    return SentryCapacitor.captureEnvelope(envelopeString);
+    return SentryCapacitor.captureEnvelope({ envelope: envelopeString });
   },
 
   /**


Test patch:
diff --git a/test/backend.test.ts b/test/backend.test.ts
index 2bde2f4..11add01 100644
--- a/test/backend.test.ts
+++ b/test/backend.test.ts
@@ -1,11 +1,14 @@
 import { CapacitorBackend } from '../src/backend';
 
-// const EXAMPLE_DSN =
-//   'https://6890c2f6677340daa4804f8194804ea2@o19635.ingest.sentry.io/148053';
+const EXAMPLE_DSN =
+  'https://6890c2f6677340daa4804f8194804ea2@o19635.ingest.sentry.io/148053';
 
 jest.mock(
   '@capacitor/core',
   () => ({
+    Capacitor: {
+      isPluginAvailable: jest.fn(() => true),
+    },
     Plugins: {
       SentryCapacitor: {
         crash: jest.fn(),
@@ -26,15 +29,14 @@ jest.mock(
 
 describe('Tests CapacitorBackend', () => {
   describe('initializing the backend', () => {
-    // TODO add these in once eventFromMessage method is in progress
-    // test('backend initializes', async () => {
-    //   const backend = new CapacitorBackend({
-    //     dsn: EXAMPLE_DSN,
-    //     enableNative: true,
-    //   });
+    test('backend initializes', async () => {
+      const backend = new CapacitorBackend({
+        dsn: EXAMPLE_DSN,
+        enableNative: true,
+      });
 
-    //   await expect(backend.eventFromMessage('test')).resolves.toBeDefined();
-    // });
+      await expect(backend.eventFromMessage('test')).resolves.toBeDefined();
+    });
 
     test('invalid dsn is thrown', () => {
       try {
@@ -47,16 +49,15 @@ describe('Tests CapacitorBackend', () => {
       }
     });
 
-    // TODO add these in once eventFromMessage method is in progress
-    // test("undefined dsn doesn't crash", () => {
-    //   expect(() => {
-    //     const backend = new CapacitorBackend({
-    //       dsn: undefined,
-    //       enableNative: true,
-    //     });
+    test("undefined dsn doesn't crash", () => {
+      expect(() => {
+        const backend = new CapacitorBackend({
+          dsn: undefined,
+          enableNative: true,
+        });
 
-    //     return expect(backend.eventFromMessage('test')).resolves.toBeDefined();
-    //   }).not.toThrow();
-    // });
+        return expect(backend.eventFromMessage('test')).resolves.toBeDefined();
+      }).not.toThrow();
+    });
   });
 });
diff --git a/test/wrapper.test.ts b/test/wrapper.test.ts
index 307b28e..3598994 100644
--- a/test/wrapper.test.ts
+++ b/test/wrapper.test.ts
@@ -21,7 +21,7 @@ jest.mock(
             version: '0.0.1',
           });
         }),
-        getStringBytesLength: jest.fn(() => Promise.resolve(1)),
+        getStringBytesLength: jest.fn(() => Promise.resolve({ value: 1 })),
         sendEvent: jest.fn(() => Promise.resolve()),
         setUser: jest.fn(() => {
           return;
@@ -117,13 +117,15 @@ describe('Tests Native Wrapper', () => {
 
       const item = JSON.stringify({
         content_type: 'application/json',
-        length: 1,
+        length: 114,
         type: 'event',
       });
 
-      await expect(NATIVE.sendEvent(event)).resolves.toMatch(
-        `${header}\n${item}\n${payload}`,
-      );
+      const response = {
+        envelope: `${header}\n${item}\n${payload}`,
+      };
+
+      await expect(NATIVE.sendEvent(event)).resolves.toMatchObject(response);
     });
 
     test('does not call Capacitor at all if enableNative is false', async () => {