Replies: 5 comments
-
Hope I put this in a right place :) AnswersCan 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 detailsWithin 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 Host appThe host app has its own 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 <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 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 appThings 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 });
});
});
},
}; SummaryAs you can see, we have reimplemented four particularly important integrations:
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.
-
Hi @smeubank Are you using Webpack’s Module Federation?Save as mlmmn mentioned, 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. |
Beta Was this translation helpful? Give feedback.
-
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:
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, 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 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 Since we aren’t using 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 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.
-
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.
-
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) Total number of components, MICROs and HOSTs (# of components) 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) 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. 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. |
Beta Was this translation helpful? Give feedback.
-
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
Original questions pre October 21st, 2022
Internal doc
https://www.notion.so/sentry/Micro-Frontends-MFE-470a081e95d941fe98b779e83518eab9
Beta Was this translation helpful? Give feedback.
All reactions