-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
-
Originally posted by smeubank February 1, 2022
We are aware of challenges customers face regarding the hub and scope model of our SDKs, and restrictions posed simply by how web browsers work globally. We want to consider ways to improve our SDK to better service impacted customers, and specifically the needs of customers with Micro Frontend style architectures. Below is a list of questions we would appreciate your feedback on, this will help guide our decision making process and engineering efforts.
A demo app with example for further help: https://github.com/realkosty/sentry-micro-frontend
- Is the HOST-application owned by another team within your company or a 3rd party/customer? (remote/library)
- Are your MICRO-components built and deployed independently or are they pulled in as package dependencies during HOST-application’s build process? (3rd party)
- Total number of devs working on front-end (# of devs)
- Total number of components, MICROs and HOSTs (# of components)
- Do you have multiple MICRO-components in your HOST-application? (# of MICROs per HOST)
- Is your MICRO-component consumed by multiple HOST-applications? (# of HOSTs per MICRO)
- Are you using Webpack Module Federation or some other framework to implement MFE? (Webpack)
- Could you connect us with someone on your team who’d be able to give details on your architecture? (In-depth Technical: Architecture Details)
- Why do you want errors from different components to go into different Sentry projects/teams/filters??? Please go into as much detail here as possible.
Original questions pre October 21st, 2022
- What does your MicroFE architecture look like?
- Can you link us to documentation or tutorial showing exactly how your micro frontend architecture works?
- Are you using Webpack’s Module Federation?
- If yes, does your usage follow any of these examples?
- How would they see Sentry hub and scope working?
- Do you know of any solutions available today that solve for this? Can you link us to the documentation?
- Would you be interested in participating in an Early Adoptor program with solutions for Hub and Scope propagation?
- Would you be comfortable with a bundle size increase from having to configure sentry for each micro-frontend (separate init calls, creating separate hubs/clients, explicit scopes etc.)?
Internal doc
https://www.notion.so/sentry/Micro-Frontends-MFE-470a081e95d941fe98b779e83518eab9
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 21
Replies: 5 comments
-
Hope I put this in a right place :)
Answers
Can you link us to documentation or tutorial showing exactly how your micro frontend architecture works?
There's no publicly available information unfortunately, but it basically follows the example from the webpack repo linked later on.
Are you using Webpack’s Module Federation?
Yes.
If yes, does your usage follow any of these examples?
Yes, the dynamic-system-host example.
For the record, we followed the example from this particular revision.
How would they see Sentry hub and scope working?
I'm, afraid I don't understand the question.
Do you know of any solutions available today that solve for this? Can you link us to the documentation?
No known solutions afaik.
Would you be interested in participating in an Early Adoptor program with solutions for Hub and Scope propagation?
Yes.
Would you be comfortable with a bundle size increase from having to configure sentry for each micro-frontend (separate init calls, creating separate hubs/clients, explicit scopes etc.)?
It depends on how big such increase would be. Code-splitting is always welcome whenever feasible :)
Architecture details
Within our React based host-remote architecture, we developed the following solution for managing Sentry with federated modules. The assumption is that the remote app means an app that is developed and maintained by an independent team. Teams are free to use any monitoring tools they wish, although as of today everyone does use Sentry indeed.
Within the Sentry's host app project we either assign teams based on tags sent with an event or via rules set in the web interface. This works to some extent, but does not allow federated apps teams to take advantage of source maps as this requires an independent Sentry project and thus...
... a dedicated Sentry client with own dsn url and release tags. Below I'll do my best to describe our way to workaround current Sentry limitations.
Host app
The host app has its own Sentry.init() call. This obviously works for every case as described in the docs (particularly the integrations).
Sentry.init({ dsn: "https://***@***.ingest.sentry.io/***", release: getReleaseTag(), allowUrls: sentryAllowUrls, denyUrls: sentryDenyUrls, ignoreErrors: sentryIgnoreErrorsList, beforeSend: function (event: Sentry.Event, hint: Sentry.EventHint) { if (beforeSendFilters.some((filter) => filter(event, hint))) { return null; } return event; }, attachStacktrace: true, sampleRate: 0.1, integrations: [new TracingIntegrations.BrowserTracing()], tracesSampleRate: 0.2, });
The federated module loading function sits in a dedicated module and is ready to be reused
export const getRemoteModule = async ( remote: AvailableRemotes, module: string ): Promise<{ default: any }> => { await __webpack_init_sharing__("default"); const container = window[remote]; await container.init(__webpack_share_scopes__.default); // If anything throws an error during initialization of remote module, it happens right here const factory = await window[remote].get(module); return factory(); };
To actually get a remote React component we use a useRemoteComponent hook which is mostly based on useDynamicScript from Webpack's example. The relevant addition is that we wrap such component within its own RemoteComponentErrorBoundary component like so:
<RemoteComponentErrorBoundary remote={remote}> <Suspense fallback={<Loader />}> <RemoteComponent {...props} /> </Suspense> </RemoteComponentErrorBoundary>
export class RemoteComponentErrorBoundary extends React.Component< Props, State > { async componentDidCatch(error: Error) { const { remote } = this.props; try { // Attempt to get remote logger const remoteLogger: RemoteLogger = ( await getRemoteModule(remote, "./logger") ).default; remoteLogger.captureException(error); } catch { // Remote logger does not exist, use host's logger and tag correct team const team = mapRemoteToTeam[remote]; logger.captureException(error, { ...(team && { team }), }); } } }
You can clearly see that we have introduced a kind of a "contract": if a team wants to use their own Sentry client, they're allowed to do so by exposing a ./logger entry that follows given interface.
We assume that this should be used only for initialization exceptions (thus the fallback for host's logger) and for the actual logging the remote app is encapsulated within own error boundary component.
Remote app
Things get much much more complicated in the remote app realm. Since Sentry's multi client support is limited, we have somewhat reverse-engineered some integrations implementation to bring the most important back.
// Sentry integrations (breadcrumbs, user agent and so on) currently do not work when using multiple clients (meaning // initializing Sentry like above, not by Sentry.init()). This is said to be fixed in Sentry client v7 according to the // GitHub issue https://github.com/getsentry/sentry-javascript/issues/2732 // Until v7 is released, some functionality like breadcrumbs and user agent info have to be manually recreated. const linkedErrors = new Integrations.LinkedErrors(); const dedupe = new Integrations.Dedupe(); let previousEvent: Event; const client = new BrowserClient({ dsn: "https://***@***.ingest.sentry.io/***", release: getEnv().RELEASE, defaultIntegrations: false, // Won't work with multiple client setup anyway attachStacktrace: true, sampleRate: 0.1, normalizeDepth: 6, beforeSend(event, hint) { // Recreating Dedupe integration to ignore duplicate events if ( shouldIgnoreEvent(event) || // @ts-ignore _shouldDropEvent is protected dedupe._shouldDropEvent(event, previousEvent) // eslint-disable-next-line no-underscore-dangle ) { return null; } previousEvent = event; // Recreating LinkedErrors integration (for React component stack trace) // @ts-ignore _handler is protected // eslint-disable-next-line no-underscore-dangle const enrichedEvent = linkedErrors._handler(event, hint) as Event; // Recreating UserAgent integration if (window.navigator && window.location && window.document) { const url = enrichedEvent.request?.url || window.location?.href; const { referrer } = window.document || {}; const { userAgent } = window.navigator || {}; const headers = { ...enrichedEvent.request?.headers, ...(referrer && { Referer: referrer }), ...(userAgent && { "User-Agent": userAgent }), }; const request = { ...(url && { url }), headers }; enrichedEvent.request = { ...enrichedEvent.request, ...request }; } enrichedEvent.breadcrumbs = enrichedEvent.breadcrumbs ?.filter((breadcrumb) => !shouldIgnoreBreadcrumb(breadcrumb)) .sort((a, b) => { if (a.timestamp && b.timestamp) { return a.timestamp - b.timestamp; } return 0; }); return enrichedEvent; }, }); const hub = new Hub(client); const getBreadcrumbs = () => // Obtain breadcrumbs from global Sentry hub instance in host app // @ts-ignore _breadcrumbs is protected getCurrentHub().getScope()._breadcrumbs ?? []; // eslint-disable-line no-underscore-dangle // The actual client that is exported export const sentry = { addBreadcrumb: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => hub.addBreadcrumb(breadcrumb, hint), setUser: (user: User) => hub.setUser(user), setTags: (tags: Tags) => hub.setTags(tags), captureGraphQLError: ( error: GraphQLError, captureContext?: CaptureContext ) => { hub.run((currentHub) => { currentHub.withScope((scope) => { scope.setContext("GraphQLError", { ...(error.path && { path: error.path }), code: error.extensions?.code, }); scope.setFingerprint( [ error.message, error.extensions?.code, ...(error.path ? error.path.map(String) : []), ].filter(Boolean) ); currentHub.captureMessage(error.message, Severity.Error, { captureContext, }); }); }); }, captureException: ( exception: Error, captureContext?: CaptureContext & { componentStack?: string } ) => { const breadcrumbs = getBreadcrumbs(); hub.run((currentHub) => { currentHub.withScope((scope) => { breadcrumbs.forEach((breadcrumb) => { scope.addBreadcrumb(breadcrumb); }); const error = exception instanceof Error ? exception : new Error((exception as Error).message || "Unknown error"); if (captureContext?.componentStack) { // Copied from https://github.com/getsentry/sentry-javascript/blob/master/packages/react/src/errorboundary.tsx const errorBoundaryError = new Error(error.message); errorBoundaryError.name = `ReactErrorBoundary: ${errorBoundaryError.name}`; // @ts-ignore used by sentry errorBoundaryError.stack = captureContext.componentStack; // @ts-ignore used by sentry error.cause = errorBoundaryError; // eslint-disable-line no-param-reassign } currentHub.captureException(error, { originalException: error, captureContext, }); }); }); }, captureMessage: ( message: string, level?: Severity, captureContext?: CaptureContext ) => { const breadcrumbs = getBreadcrumbs(); hub.run((currentHub) => { currentHub.withScope((scope) => { breadcrumbs.forEach((breadcrumb) => { scope.addBreadcrumb(breadcrumb); }); currentHub.captureMessage(message, level, { captureContext }); }); }); }, };
Summary
As you can see, we have reimplemented four particularly important integrations:
BreadcrumbsLinkedErrors(for React stack)DedupeUserAgent
In an ideal setup, we'd like to have this integrations available out of the box in the remote app. In the remote app we don't care about uncaught exceptions since it's host's job to catch those.
Beta Was this translation helpful? Give feedback.
All reactions
-
🚀 7
-
Hi @smeubank
We are facing the same challenges when applying sentry to our MFE architecture.
I'm Interested in solution for using sentry in MFE context and also interested in participating in an Early Adoptor program with solutions for Hub and Scope propagation.
Are you using Webpack’s Module Federation?
Save as mlmmn mentioned,
we also use module federation.
If yes, does your usage follow any of these examples?
Would you be interested in participating in an Early Adopter program with solutions for Hub and Scope propagation?
Yes.
Problem we are facing:
For remote app error capture, we export the sentry hub logic from remote to host.
Then use this hub captureException to capture error in remote module. Just like the process @mlmmn mentioned above.
But the actual behavior is the error in the remote module is first captured by the host sentry instance ( with default integrations )
Since the error is captured by host sentry instance, remote hub will not send the event.
Beta Was this translation helpful? Give feedback.
All reactions
-
What does your MicroFE architecture look like?
When a user requests a page they are delivered a basic HTML page that acts as a "shell". Each such request results in a full page refresh. This HTML page includes script tags for loading 2 important javascript entrypoints:
- Some "global" javascript that should be executed on every page
- Some "page specific" javascript for that particular page. This javascript differs for each page and is primarily "owned" and deployed independently by one of many teams. This javascript could be as simple as running some vanilla javascript, or as complex as mounting Vue/React apps to the DOM.
Since each team is responsible for their page specific javascript, we would like to have individual projects in Sentry for each for catching errors that originate in that bundle only.
On top of all of this, we have recently started to use MFEs via Webpack’s ModuleFederation. From either of the two entry points mentioned above, we dynamically load remote MFEs. These MFEs are independently deployed and again have strong team ownership. Similarly, we would like to capture all the errors that occur within a given MFE and report out to a specific project in Sentry.
For any particular page, we might have 5 or more MFEs all living on the page at once. For example on one page we have 3 Vue apps and 1 React app running in harmony. Each owned by a different team.
How have we tried to make this work with Sentry’s current capabilities?
As we understand it, Sentry.init() sets up error monitoring for the entire page to report out to one DSN. This is not what we want, and as a result we avoid using it. We need many DSNs to report out to depending on where javascript errors are encountered. For the moment we have relied on direct use of the BrowserClient. Each app is responsible for constructing its own BrowserClient and providing it with the proper DSN.
For MFEs since we are typically using Vue or React, we can use Vue’s error handler or error boundaries in React to capture errors that occur within their runtime. This works okay, but we lose a lot of the nice integrations that can supplement the context info that is included with the error. For example we tried to use the Vue Integration but encountered issues.
For errors that don’t occur in our MFEs, we have set up a global listener on the window for error events. By inspecting the filename from which the error came, we can make decisions about which DSN to send the error to. This works okay for the most part, but we have had many headaches along the way.
We would love to also have some sort of fallback error handler that catches some error that occurs on the page, but hasn’t been picked up by any other BrowserClient on the page yet.
Since we aren’t using Sentry.init(), we can’t leverage Sentry’s page performance capabilities which we are very interested in. Collecting page performance metrics only makes sense to capture at the page level, not the individual MFE/bundle level. Would be very interested in still being able to route errors to specific projects in Sentry while also being able to view general page performance metrics somewhere.
Can you link us to documentation or tutorial showing exactly how your micro frontend architecture works?
The most useful link I can provide is a tutorial that we took inspiration from for our MFE architecture: https://www.udemy.com/course/microfrontend-course/ . One key demonstration in this tutorial is placing MFEs built in different frontend frameworks (React, Vue, etc.) all living on the same page.
Are you using Webpack’s Module Federation?
Yup!
If yes, does your usage follow any of these examples?
Closest of those examples might be the dynamic-system-host.
When consuming an MFE we do not specify the list of remotes via the ModuleFederationPlugin. Instead we dynamically load the remoteEntry.js files via a backend service at runtime using ModuleFederation’s low level API.
How do you see Sentry hub and scope working?
Really not sure. Maybe something similar to what we have tried where there is some globally registered utility setup on the page one time, that the rest of the BrowserClients can interact with?
While we would like to have individual sentry projects we still highly value the context data that is provided along with each reported error (e.g. breadcrumbs, user agent, etc.). Having to replicate that logic ourselves is a pain.
Do you know of any solutions available today that solve for this? Can you link us to the documentation?
No
Would you be interested in participating in an Early Adopter program with solutions for Hub and Scope propagation?
Sure!
Would you be comfortable with a bundle size increase from having to configure sentry for each micro-frontend (separate init calls, creating separate hubs/clients, explicit scopes etc.)?
Yup comfortable with that. Especially since with ModuleFederation we’d be able to share the Sentry dependency across all MFEs so that it’s only loaded once per page.
Beta Was this translation helpful? Give feedback.
All reactions
-
Hello 👋
We have not forgotten! This topic is a bit complex and will take alignment across more than just our SDK team at Sentry.
What the team did do was come up with some possible topics for 2023. Point 2 would be with regards to MFE and exception isolation. I am also updating the questions, to give more of a product experience questions. Questions provided with help from @realkosty
Beta Was this translation helpful? Give feedback.
All reactions
-
👀 1
-
This is kind of different from other issues but faces the same problem. We are developing a Web component used by our customers.
Is the HOST-application owned by another team within your company or a 3rd party/customer? (remote/library)
Customer
Are your MICRO-components built and deployed independently or are they pulled in as package dependencies during HOST-application’s build process? (3rd party)
Built independently (for now)
Total number of devs working on front-end (# of devs)
2
Total number of components, MICROs and HOSTs (# of components)
1, lots of web components but only the "root" component uses Sentry
Do you have multiple MICRO-components in your HOST-application? (# of MICROs per HOST)
Don't know. Up to customer
Is your MICRO-component consumed by multiple HOST-applications? (# of HOSTs per MICRO)
Yes hundreds
Are you using Webpack Module Federation or some other framework to implement MFE? (Webpack)
No, we use Lit with Vite
Could you connect us with someone on your team who’d be able to give details on your architecture? (In-depth Technical: Architecture Details)
Sure
Why do you want errors from different components to go into different Sentry projects/teams/filters??? Please go into as much detail here as possible.
We don't control the host as it belongs to our customers. They may be running Sentry as well using any of the existing integration methods.
Obvioulsy we do not want any errors logged happening outside our component.
Beta Was this translation helpful? Give feedback.
All reactions
-
👀 1