npm package: @personio/alerting
Description: A package to report frontend errors on sentry
Latest version: 0.2.5

# Alerting

> An npm package for micro-frontends to report frontend errors to Sentry

## About

This packages leverages the [BrowserClient][1] and [Hub][1] APIs to create a
Sentry instance dedicated to the micro-frontend it is running on.

Unlike the `Sentry.init` API, this ensures that in a webpage with multiple
micro-frontends, we can have multiple instances of Sentry each dedicated to one
micro-frontend. Behind the scenes, this package will ensure that only the errors
occurring from the micro-frontend bundle the Sentry instance is running on get
reported to the Sentry project this Sentry instance is tied to.

For example, a webpage with two micro-frontends `MFE-A` and `MFE-B` each having
their own Sentry instance `Sentry-A` and `Sentry-B` where `MFE-A` throws an
error on a click event, the internal working of these Sentry instances will
ensure that `Sentry-A` reports this event on the `MFE-A` Sentry project while
`Sentry-B` will simply ignore this error.

## Installation

Install the package:

```sh
yarn add @personio/alerting
```

:warning: **Note!** :warning:

`@personio/alerting` already comes with both `@sentry/browser` and `@sentry/cli`
bundled as dependencies. There is no need to have them in your project, so feel
free to remove them if you do.

```sh
yarn remove @sentry/browser @sentry/cli
```

## Minimal Usage

First, you need to add the `crossorigin` attribute to your bundle's `script` tag
and set its value to `anonymous`. This is what allows us to detect which JS file
an error originated from.

**For mFEs not in use on production:**

```html
<script src="<cdn>/my-mfe/8BADF00D.js" crossorigin="anonymous" defer></script>
```

**For mFEs in use on production for at least one customer:**

**Note that we are adding a queryParam to the src of the script tag called
`crossOriginCacheBuster`.** We need to do this to make sure that when this code
goes on production, the cached JS file for the mFE is invalidated by the browser
and instead it is fetched from the CDN. This will ensure that the response from
CDN has the ACAO headers set correctly

```html
<script
  src="<cdn>/my-mfe/8BADF00D.js?crossOriginCacheBuster"
  crossorigin="anonymous"
  defer
></script>
```

Next, the minimal implementation is to wrap your application with the package's
`SentryProvider` component, and pass the necessary Sentry config options via
props.

```js
import { SentryProvider } from '@personio/alerting';

…

<SentryProvider
  dsn={SENTRY_DSN}
  env={ENV}
  name={MICRO_FRONTEND_NAME}
  version={MICRO_FRONTEND_VERSION}
  enabled={IS_SENTRY_ENABLED}
>
  <App />
</Sentryprovider>
```

### Potential issues

- if the webpack dev server is not setup properly, while consuming local
  instance of mFE from local Monolith the following error may appear:

  `Access to script at ... from origin ... has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.`

  In order to resolve it, make sure that webpack dev server is configured in way
  so it works properly with CORS headers, which means that
  `build-utils/webpack.development.js` (or similar) file in the mFE must have
  `devServer.headers` conf section which includes
  `'Access-Control-Allow-Origin': '*.dev.personio.de',`.

## In what order should I deploy the changes in the monolith and my mFE

You should first merge the MR on the monolith with the newly added `crossorigin`
attribute

Once the monolioth deployment has finished, you can go ahead and deploy your mFE

## Integration with Error Boundary

The package also provides a`SentryErrorBoundary` component which you can use in
your MFE to automatically catch and report rendering errors to your Sentry
instance.

```js
import { SentryErrorBoundary } from '@personio/alerting';

…

<SentryErrorBoundary errorComponent={<div>Something went wrong</div>}>
  <TemperamentalComponent />
</SentryErrorBoundary>
```

If you wish to only use this as an ErrorBoundary and _not_ have it report to
Sentry, you can pass `false` to the `reportRenderError` prop.

```js
import { SentryErrorBoundary } from '@personio/alerting';

…

<SentryErrorBoundary
  errorComponent={<div>Something went wrong</div>}
  reportRenderError={false}
>
  <TemperamentalComponent />
</SentryErrorBoundary>
```

## Manually Capturing Exceptions

`@personio/alerting` also exposes the internal method used to capture exceptions
through the Context, allowing you to manually capture your own exceptions where
needed. You can optionally pass `extras` which will appear alongside your alert
in Sentry, with default `environment`, `mfe_name`, and `mfe_version` values.

**_Note:_** this is preferred to using the Sentry Hub instance directly.

```js
import { nanoid } from 'nanoid';
import { useSentry } from '@personio/alerting';

const PokemonDetails = () => {
  const { capture } = useSentry();

  const fetchPokemon = (id) => {
    let data = {};
    try {
      const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
      data = await res.json();
    } catch (err) {
      // `capture` checks if Sentry is enabled internally
      capture(err, { uuid: nanoid() });
    }
    return data;
  };
};
```

## Accessing the Sentry Hub

We leverage React Context under-the-hood to store an instance of [Sentry Hub][2]
and handle the reporting. This Hub and Context are available as an escape hatch,
a means of directly reporting from custom locations, or to further [enrich
events][3] with additional info.

The Hub (and your Sentry instance's `enabled` status) is exposed through a
custom hook that can be consumed anywhere in your app that hooks can normally be
used.

**_Note:_** this is entirely optional and, typically, you will never use it.

```js
import { nanoid } from 'nanoid';
import { useSentry } from '@personio/alerting';

const PokemonDetails = () => {
  const { enabled, hub } = useSentry();

  const fetchPokemon = (id) => {
    let data = {};
    try {
      const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
      data = await res.json();
    } catch (err) {
      if (enabled) {
        hub.setTag('uuid', nanoid());
        hub.captureException(err);
      }
    }
    return data;
  };
};
```

[1]:
  https://docs.sentry.io/platforms/javascript/guides/react/troubleshooting/#using-a-client-directly
[2]: https://docs.sentry.io/platforms/javascript/enriching-events/scopes/
[3]: https://docs.sentry.io/platforms/javascript/enriching-events/