getsentry / sentry-javascript

Official Sentry SDKs for JavaScript
https://sentry.io
MIT License
7.76k stars 1.52k forks source link

Isolated scopes in Micro Frontends/MFE/Microfrontends #12314

Open johnmbauan opened 1 month ago

johnmbauan commented 1 month ago

Problem Statement

I'm working in a context where we have several micro-frontends, and we want these micro-frontends to be completely independent in terms of how sentry is initialized and keep the different instances running in any given page isolated. We're not setup for Module Federation so we cannot follow what is suggested here.

I've followed what's described in this page: created a scope, created a BrowserClient and use scope.captureException.

    const client = new BrowserClient({
        dns: 'http://...',
        transport: makeFetchTransport,
        stackParser: defaultStackParser,
        integrations: [...getDefaultIntegrations({}), vueIntegration({ app })], //note the usage of vueIntegration
    });

    //create a new scope for this instance of Sentry, to isolate it from other sentry instances
    const isolatedScope = new Scope();
    isolatedScope.setClient(client);
    client.init();

Manually capturing exceptions with isolatedScope.captureException works well, as the events are sent to the correct Sentry project. However, if an uncaught exception is thrown somewhere in the Vue app, nothing happens. I've investigated the codebase and found that the errorHandler defined in the Vue integration doesn't account for any isolated scope created outside of the vue integration init function. This means that any exceptions that are not explicitly handled with isolatedScope.captureException will be handled using whatever is the current scope.

Solution Brainstorm

I propose making the option of the init function accept an additional "useIsolatedScope": boolean; field, based on which the error handler that is injected into the Vue app will use a new isolated scope to capture exceptions. I'm sure it's not that simple, but the high-level idea is to simplify the setup of an isolated sentry instance with Vue.

(I have a working solution that somewhat resembles what I'm proposing, though I went lower level by using a modified version of attachErrorHandler directly on the Vue app and passing the scope to it)

lforst commented 1 month ago

Hi, unfortunately it is indeed not that simple because the browser lacks AsyncLocalStorage (or basically any functionality to store data for an async execution context) which is what we would use to avoid leakage between scopes.

You mentioned that you are not using module federation. Module federation is not a requirement to use our Micro Frontend guide: https://docs.sentry.io/platforms/javascript/best-practices/micro-frontends/ As long as you have separate builds for each of your micro frontends you can use the solution we propose there.

johnmbauan commented 1 month ago

Thanks @lforst . Based on my understanding, the solution proposed in https://docs.sentry.io/platforms/javascript/best-practices/micro-frontends/ would require some centralization of the initialization of Sentry, which we'd like to avoid. Is my understanding correct?

In all cases Sentry.init() must never be called more than once, doing so will result in undefined behavior.

lforst commented 1 month ago

So in order to catch global errors (ie. errors that bubble up to window.onerror) you need some kind of centralization, since you would need to determine which client/DSN should capture the error.

As it stands, I don't think there is a way around having at least one global (fallback) client that does routing based on some heuristic. Lmk if you have any suggestions.

jbauan-te commented 1 month ago

As it stands, I don't think there is a way around having at least one global (fallback) client that does routing based on some heuristic.

Gotcha, that makes sense.

How do things work with in the context of multiple clients initialized through these steps ? Which is the sentry instance that would catch global errors? 🤔

Anyways, I think in the context of multiple remote MFEs in a host MFE (i'm using module federation terms here), each remote MFE would really be interested in catching errors originating form "itself" (i.e. its root Vue/React/Angular "application" instance); any other error should be caught by the host MFE. In this context, all the remote MFEs would initialize the sentry instance using an isolated scope, while the host MFE would initialize sentry in the standard way (and would act as the default error handler). Would that be possible? This would allow for fully independent sentry initialization, avoiding for centralization that would need to be repeated in each host MFE.

lforst commented 1 month ago

Ideally, you have one client that uses the default integrations via getDefaultIntegrations(). This client then catches all global errors (ie errors from handlers, window.onerror, setTimeout etc) and routes them to the right DSN with the process described here.

Then, additionally, you can have clients that can be used to manually capture errors via client.captureException(error). You need to lose the idea of "scope". Scope is not possible in the browser due to the lack of an AsyncLocalStorage API. Without such an API, errors will leak all over the place and between your projects when you have async stuff happening (which is basically all the time in a real-world scenario).

The only thing you should absolutely not do is call Sentry.init() more than once, or set up multiple clients with the default integrations.

In my descriptions, I will intentionally not mention anything about module federation or any other type of frameworks, since in my experience everybody does MFEs differently and finding specific solutions is pointless.