How to use Sentry in micro frontend

Amir Ajorlou
4 min readApr 4, 2023

--

Probably if you are working on a micro frontend project and want to have a dedicated Sentry project for each app, you realize that there is no easy way to do it and after your search, you may see this comment:

A comment in response to a similar issue

After searching and reading the Sentry source code, I found a solution (workaround) until we have official micro frontend support in Sentry.

I developed this solution with Single-SPA and React, but it should also be very similar to other frameworks.

Context

The “normal way” works perfectly when you have only one Sentry project and it’s really easy to setup:

Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",

// Alternatively, use `process.env.npm_package_version` for a dynamic release version
// if your build tool supports it.
release: "my-project-name@2.3.12",
integrations: [new BrowserTracing()],

// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
});

But the problem starts when you have more than one Sentry. You might think that you can init a dedicated Sentry project in each app and you will filter events by allowUrls and denyUrls and everything should work, but NO!

After using Sentry.init , it prevents you from init another one. This action also adds some properties to the window and adds two event listeners to it:

  1. onerror: This will be fired on sync errors.
  2. unhandledrejection: This will be fired on every unhandled error that your code will get in async functions.

This means that with this setup you can only have one Sentry.

Idea

This post in the Sentry forum gave me an idea of how to implement it. As you can see in this example code, it’s using the hub and client instead of Sentry.init. So there is no global event listener and working with window.

To capture errors, it wraps the code with ErrorBoundary and sending errors manually using hub:

componentDidCatch(error, errorInfo) {
this.props.hub.run(currentHub => {
currentHub.withScope((scope) => {
scope.setExtras(errorInfo);
const eventId = currentHub.captureException(error);
this.setState({eventId});
});

})
}

This method is working well for sync errors, but it cannot catch the errors that happen in async functions, like Promises, because:

Note

Error boundaries do not catch errors for:

Event handlers (learn more)

Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)

Server side rendering

Errors thrown in the error boundary itself (rather than its children)

There are some ways to capture async errors in ErrorBoundary, like using a custom wrapper as mentioned in this post, but I prefer to not show the error fallback page because of an unhandled error in an async function.

Solution

As I mentioned in the previous section, the main problem is catching “unhandled async errors”. I know it’s best to wrap all async functions to capture errors and add context to them, but it’s good to also have a listener to capture all the unhandled ones.

I created a simple package to make this setup easier. You can find the package here or the Github repo here.

First, we create the hub and client instead of using Sentry.init.

import { MFESentry } from 'mfe-sentry'

MFESentry.createClient({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
integrations: [new BrowserTracing()],
transport: makeFetchTransport,
stackParser: defaultStackParser,
tracesSampleRate: 1.0,
})

Then, to catch sync errors, we add an ErrorBoundary component and customize the componentDidCatch, this can be different based on the framework (for example in Single-SPA, you can pass a function as errorBoundary and get the error and render the fallback), but you can also add a normal ErrorBoundary and wrap the project with it.

/// MyErrorBoundaryExample.ts

import React from 'react'

import { MFESentry } from 'mfe-sentry'

class MyErrorBoundaryExample extends React.Component {
state = {
error: null,
};

static getDerivedStateFromError(error) {
// Update state so next render shows fallback UI.
return { error: error };
}

componentDidCatch(error) {
MFESentry.captureExecption(error)
}

render() {
if (this.state.error) {
// You can render any custom fallback UI
return <p>Something broke</p>;
}
return this.props.children;
}
}

export default MyErrorBoundaryExample

/// index.ts
import React from 'react';
import ReactDOM from 'react-dom/client';

import App from './App';
import MyErrorBoundaryExample from './MyErrorBoundaryExample'

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<MyErrorBoundaryExample>
<App />
</MyErrorBoundaryExample>
</React.StrictMode>
);

To catch async functions’ errors, You need to set a listener to unhandledrejection event:

/// App.ts

useEffect(() => {
window.addEventListener(
'unhandledrejection',
MFESentry.handleUnhandledRejection,
)

return () => {
window.removeEventListener(
'unhandledrejection',
MFESentry.handleUnhandledRejection,
)
}
}, [])

Also, if you want to capture exceptions, events, or messages manually, you can use these three functions:

import { MFESentry } from 'mfe-sentry'

MFESentry.captureMessage(
error: string,
severity?: SeverityLevel,
hint?: EventHint,
)

MFESentry.captureEvent(error: Error, hint?: EventHint)

MFESentry.captureExecption(
error: any,
hint?: EventHint,
forceSendingNetworkError?: boolean,
)

Sentry has a couple of functions that it’s using to prepare the events to capture them, but unfortunately, they are not exported, so I had to copy them to use them.

Hope this post helped you to have a better setup for your project!

--

--