Frontend errors: use Sentry Integration for error logging

# Overview

As part of our effort to improve the frontend logs management in `CustomersDot`, as well as reduce code duplication and manual error handling, provide more useful and consistent error info to our users, we will implement a [_holistic_ approach to errors](https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/2279) which consists of 2 parts:

- Use Sentry Integration for error logging (current)
- [Use `ErrorBoundary` for error handling](https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/3350)

The 2 parts have been individuated in a **[previous research with PoC](https://gitlab.com/gitlab-org/customers-gitlab-com/-/merge_requests/2935)** which helped define the approach. Some use cases are presented [here](https://vue-demo.xyz/).

##### Use Sentry Integration for error logging

We expect improvements in the following areas:

- separation of concerns: our components will only be responsible for *handling* errors, not for *logging* them
- DRYness: in most cases where we manually log errors, we're just proxying them. This only adds maintenance cost and no value
- using [boring solutions](https://about.gitlab.com/handbook/values/#boring-solutions): we _already_ have most of the functionality in place, due to Sentry Integration being active [the following line](https://gitlab.com/gitlab-org/customers-gitlab-com/-/blob/50ee3275a0177e192102268f93ce87f47ee07d8b/app/frontend/packs/sentry.js#L9)

We will measure them by:

- reduction or elimination of manual calls to `Sentry.captureMessage` and `Sentry.captureException` most of which are just proxying the error ([instances of `captureMessage`](https://gitlab.com/search?group_id=9970&project_id=2670515&repository_ref=&scope=blobs&search=Sentry.captureMessage&snippets=false) and [instances of `captureException`]((https://gitlab.com/search?group_id=9970&project_id=2670515&repository_ref=&scope=blobs&search=Sentry.captureException&snippets=false)))
- reduction or elimination of the checks for Sentry call in tests

##### Scenarios to consider

<details>
<summary>Errors based on [unprocessable entity](https://www.keycdn.com/support/422-unprocessable-entity)</summary>

We have had [some cases as such](https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/2697). Errors like this are very useful for the user in order to recover from them but may or *may not* be useful for our system. A centralized approach will let us configure which error we want to exclude at a higher level.
</details>

<details>
<summary>Dynamic rendering errors</summary>

Rendering errors are hard to handle mainly because of unpredictability: how would you know which component is gonna break and for which reason? It seems intuitive that only a centralized way can give enough confidence for such cases. Also note: by *dynamic* rendering errors here we mean the ones that might be caused by supplying inconsistent data to a component. Errors that prevent the component from rendering, whatever the point of failure is (a method, the template, etc), the kind of scenarios that would leave the page blank and log a red error in the console.
</details>

# Implementation plan

Vue offers an [errorHanlder config](https://vuejs.org/v2/api/#errorHandler) which can be used as a unified place to where all errors can bubble up. [Sentry Vue Integration](https://docs.sentry.io/platforms/javascript/guides/vue/configuration/integrations/default/) [attaches the `errorHandlder` to our Vue instance](https://github.com/getsentry/sentry-javascript/blob/master/packages/integrations/src/vue.ts#L390). This will catch and log all the errors generated at runtime to the specified Sentry API.

In order to make this work we will need to:

* always `return` `Promise` from Vue `methods` where async calls are made (if we don't do it, Sentry Integration will catch the error anyways, but it will also display as an uncaught error in the console)
* if the error is manually handled (in a `catch` block), always throw a new `Error`

##### Sentry.captureException

An example of how one can remove the manual `captureException` and let Sentry handle the error:

<details>
<summary>Diff</summary>

```
diff --git a/app/frontend/javascripts/subscription/components/subscription_license_buttons.vue b/app/frontend/javascripts/subscription/components/subscription_license_buttons.vue
--- a/app/frontend/javascripts/subscription/components/subscription_license_buttons.vue	(revision d2679d3c9f25ef8ac0a57a3e1e9f875f1169fa2c)
+++ b/app/frontend/javascripts/subscription/components/subscription_license_buttons.vue	(date 1626185697875)
@@ -49,7 +49,7 @@
       if (this.licenseFile.length > 0) {
         return;
       }
-      this.fetchLicense();
+      return this.fetchLicense();
     },
     async fetchLicense() {
       this.isFetchingLicense = true;
@@ -64,7 +64,7 @@
         this.isLicenseCollapseVisible = false;
         // eslint-disable-next-line no-jquery/no-animate-toggle
         this.$toast.show(this.$options.toast.error);
-        Sentry.captureException(err);
+        throw new Error(err.message);
       }
       this.isFetchingLicense = false;
     },
```
</details>

The resulting payload sent to Sentry is the same:

<details>
<summary>Before</summary>
```json
{"exception":{"values":[{"type":"Error","value":"Request failed with status code 422","stacktrace":{"frames":[{"colno":7,"filename":"https://agulina.ngrok.io/packs/js/vendor-b73adfbd1839150fb479.chunk.js","function":"XMLHttpRequest.handleLoad","in_app":true,"lineno":36629},{"colno":12,"filename":"https://agulina.ngrok.io/packs/js/vendor-b73adfbd1839150fb479.chunk.js","function":"settle","in_app":true,"lineno":37389},{"colno":15,"filename":"https://agulina.ngrok.io/packs/js/vendor-b73adfbd1839150fb479.chunk.js","function":"createError","in_app":true,"lineno":37155}]},"mechanism":{"handled":true,"type":"generic"}}]},"level":"error","event_id":"783d557d0b2d40608d3ed88d33a14c99","platform":"javascript","sdk":{"name":"sentry.javascript.browser","packages":[{"name":"npm:@sentry/browser","version":"5.21.1"}],"version":"5.21.1","integrations":["InboundFilters","FunctionToString","TryCatch","Breadcrumbs","GlobalHandlers","LinkedErrors","UserAgent","RewriteFrames","Dedupe","ExtraErrorData","Vue"]},"timestamp":1626186230.9387002,"environment":"development","release":"d2679d3c9f25ef8ac0a57a3e1e9f875f1169fa2c","user":{"id":6,"email":"angelo@thismail.com","company":"Angelo Local Company"},"breadcrumbs":[{"timestamp":1626185805.2841,"category":"console","data":{"arguments":["You are running Vue in development mode.\nMake sure to turn on production mode when deploying for production.\nSee more tips at https://vuejs.org/guide/deployment.html"],"logger":"console"},"level":"info","message":"You are running Vue in development mode.\nMake sure to turn on production mode when deploying for production.\nSee more tips at https://vuejs.org/guide/deployment.html"},{"timestamp":1626185805.2847002,"category":"ui.click","message":"div.collapse-content > div#collapse-container-id-5.gl-mt-2.collapse"},{"timestamp":1626185805.5453,"category":"xhr","data":{"method":"GET","url":"https://webpacko-dev-server.ngrok.io/sockjs-node/info?t=1626185805285","status_code":200},"type":"http"},{"timestamp":1626185806.3127003,"category":"xhr","data":{"method":"POST","url":"/mini-profiler-resources/results","status_code":200},"type":"http"},{"timestamp":1626185806.3188,"category":"xhr","data":{"method":"POST","url":"/mini-profiler-resources/results","status_code":200},"type":"http"},{"timestamp":1626185806.3203,"category":"xhr","data":{"method":"POST","url":"/mini-profiler-resources/results","status_code":200},"type":"http"},{"timestamp":1626185806.3252,"category":"ui.click","message":"tbody > tr > td.profiler-duration. > a.profiler-queries-show"},{"timestamp":1626186226.4326,"category":"ui.click","message":"div > div > div > a.gl-link"},{"timestamp":1626186230.9316,"category":"xhr","data":{"method":"GET","url":"/licenses/A-S00067289","status_code":422},"type":"http"}],"request":{"url":"https://agulina.ngrok.io/subscriptions","headers":{"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"}},"contexts":{"Error":{"message":"[undefined]","name":"[undefined]","description":"[undefined]","number":"[undefined]","fileName":"[undefined]","lineNumber":"[undefined]","columnNumber":"[undefined]","stack":"[undefined]","config":{"url":"/licenses/A-S00067289","method":"get","headers":"[Object]","transformRequest":"[Array]","transformResponse":"[Array]","timeout":0,"adapter":"[Function: xhrAdapter]","xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","maxContentLength":-1,"maxBodyLength":-1,"validateStatus":"[Function: validateStatus]","data":"[undefined]"},"code":"[undefined]"}}}
```
</details>

<details>
<summary>After</summary>
```json
{"exception":{"values":[{"type":"Error","value":"Request failed with status code 422","stacktrace":{"frames":[{"colno":7,"filename":"https://agulina.ngrok.io/packs/js/vendor-b73adfbd1839150fb479.chunk.js","function":"XMLHttpRequest.handleLoad","in_app":true,"lineno":36629},{"colno":12,"filename":"https://agulina.ngrok.io/packs/js/vendor-b73adfbd1839150fb479.chunk.js","function":"settle","in_app":true,"lineno":37389},{"colno":15,"filename":"https://agulina.ngrok.io/packs/js/vendor-b73adfbd1839150fb479.chunk.js","function":"createError","in_app":true,"lineno":37155}]},"mechanism":{"handled":true,"type":"generic"}}]},"level":"error","event_id":"783d557d0b2d40608d3ed88d33a14c99","platform":"javascript","sdk":{"name":"sentry.javascript.browser","packages":[{"name":"npm:@sentry/browser","version":"5.21.1"}],"version":"5.21.1","integrations":["InboundFilters","FunctionToString","TryCatch","Breadcrumbs","GlobalHandlers","LinkedErrors","UserAgent","RewriteFrames","Dedupe","ExtraErrorData","Vue"]},"timestamp":1626186230.9387002,"environment":"development","release":"d2679d3c9f25ef8ac0a57a3e1e9f875f1169fa2c","user":{"id":6,"email":"angelo@thismail.com","company":"Angelo Local Company"},"breadcrumbs":[{"timestamp":1626185805.2841,"category":"console","data":{"arguments":["You are running Vue in development mode.\nMake sure to turn on production mode when deploying for production.\nSee more tips at https://vuejs.org/guide/deployment.html"],"logger":"console"},"level":"info","message":"You are running Vue in development mode.\nMake sure to turn on production mode when deploying for production.\nSee more tips at https://vuejs.org/guide/deployment.html"},{"timestamp":1626185805.2847002,"category":"ui.click","message":"div.collapse-content > div#collapse-container-id-5.gl-mt-2.collapse"},{"timestamp":1626185805.5453,"category":"xhr","data":{"method":"GET","url":"https://webpacko-dev-server.ngrok.io/sockjs-node/info?t=1626185805285","status_code":200},"type":"http"},{"timestamp":1626185806.3127003,"category":"xhr","data":{"method":"POST","url":"/mini-profiler-resources/results","status_code":200},"type":"http"},{"timestamp":1626185806.3188,"category":"xhr","data":{"method":"POST","url":"/mini-profiler-resources/results","status_code":200},"type":"http"},{"timestamp":1626185806.3203,"category":"xhr","data":{"method":"POST","url":"/mini-profiler-resources/results","status_code":200},"type":"http"},{"timestamp":1626185806.3252,"category":"ui.click","message":"tbody > tr > td.profiler-duration. > a.profiler-queries-show"},{"timestamp":1626186226.4326,"category":"ui.click","message":"div > div > div > a.gl-link"},{"timestamp":1626186230.9316,"category":"xhr","data":{"method":"GET","url":"/licenses/A-S00067289","status_code":422},"type":"http"}],"request":{"url":"https://agulina.ngrok.io/subscriptions","headers":{"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36"}},"contexts":{"Error":{"message":"[undefined]","name":"[undefined]","description":"[undefined]","number":"[undefined]","fileName":"[undefined]","lineNumber":"[undefined]","columnNumber":"[undefined]","stack":"[undefined]","config":{"url":"/licenses/A-S00067289","method":"get","headers":"[Object]","transformRequest":"[Array]","transformResponse":"[Array]","timeout":0,"adapter":"[Function: xhrAdapter]","xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","maxContentLength":-1,"maxBodyLength":-1,"validateStatus":"[Function: validateStatus]","data":"[undefined]"},"code":"[undefined]"}}}
```
</details>


##### Sentry.captureMessage

An example of how one can remove the manual `captureMessage` and let Sentry handle the error. **Note that this will change the log level from `info` to `error`**.

<details>
<summary>Diff</summary>

```
diff --git a/app/frontend/javascripts/subscription/components/new_gitlab_subscription.vue b/app/frontend/javascripts/subscription/components/new_gitlab_subscription.vue
--- a/app/frontend/javascripts/subscription/components/new_gitlab_subscription.vue	(revision d2679d3c9f25ef8ac0a57a3e1e9f875f1169fa2c)
+++ b/app/frontend/javascripts/subscription/components/new_gitlab_subscription.vue	(date 1626187067985)
@@ -82,9 +82,6 @@
       this.showSuccessModal = true;
     },
     handleError(message) {
-      if (message) {
-        Sentry.captureMessage(message);
-      }
       this.errorMessage = message;
     },
   },
diff --git a/app/frontend/javascripts/subscription/components/new_gitlab_subscription_form.vue b/app/frontend/javascripts/subscription/components/new_gitlab_subscription_form.vue
--- a/app/frontend/javascripts/subscription/components/new_gitlab_subscription_form.vue	(revision d2679d3c9f25ef8ac0a57a3e1e9f875f1169fa2c)
+++ b/app/frontend/javascripts/subscription/components/new_gitlab_subscription_form.vue	(date 1626187443274)
@@ -151,7 +151,7 @@
 
       this.clearError();
 
-      purchaseSubscription(requestParams)
+      return purchaseSubscription(requestParams)
         .then(({ invoice_item, invoice }) => {
           this.invoice = invoice;
           this.serviceStartDate = invoice_item[0].service_start_date;
@@ -159,6 +159,7 @@
         })
         .catch((error) => {
           this.$emit('error', error.message);
+          throw new Error('This is a dummy error')
         })
         .finally(() => {
           this.isLoading = false;
```
</details>

# Other resources

- the Draft MR: gitlab-org/customers-gitlab-com!2935
- a video showing the approaches in the demo app: [link to drive](https://drive.google.com/file/d/1yBdT51vIP-DEdL54QOZoIgPSFSOa-rhu/view?usp=sharing)
- a video for some more specific cases: [link to drive](https://drive.google.com/file/d/1KXNUX-CCU-MXot0q6aC9fic9fY7BjspR/view?usp=sharing)
- a video for some more specific cases: [link to drive](https://drive.google.com/file/d/1RBaJ7bDezuXcJblWgImY9uTDdCfeF3TS/view?usp=sharing)

Story points: 5.0