-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Simplify/reorganize Micro Frontend Suport #9215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
3b53334
55de202
09ca16b
9573613
b7a817e
bc35c2c
dbff8f2
5bd1a2f
da2820e
4511e1e
8954cd2
1144e30
cfe4745
bdbc5fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -14,18 +14,27 @@ keywords: | |||||
| If your frontend includes JavaScript bundles from multiple sources with | ||||||
| different release cycles, you may want to identify these or route events to specific projects. This is especially useful if you've set up [module federation](https://module-federation.github.io/) or a similar frontend architecture. | ||||||
|
|
||||||
| ## Identifying the source of errors | ||||||
| ## Automatically route errors to different projects depending on module | ||||||
|
|
||||||
| To identify the source of an error, you must inject metadata that helps identify | ||||||
| which bundles were responsible for the error. You can do this with any of the | ||||||
| Sentry bundler plugins by enabling the `_experiments.moduleMetadata` option. | ||||||
| `ModuleMetadata` and `makeMultiplexedTransport` can be used together to automatically route | ||||||
| events to different Sentry projects based on the module where the error | ||||||
| occurred. | ||||||
|
|
||||||
| <Note> | ||||||
| <ul> | ||||||
| <li> | ||||||
| Requires version `2.5.0` or higher of `@sentry/webpack-plugin` or version | ||||||
| `2.7.0` or higher of `@sentry/rollup-plugin`, `@sentry/vite-plugin` or `@sentry/esbuild-plugin`. | ||||||
| </li> | ||||||
| <li> | ||||||
| Requires SDK version `7.59.0` or higher. | ||||||
| </li> | ||||||
| </ul> | ||||||
| </Note> | ||||||
|
|
||||||
| `moduleMetadata` can be any serializable data or alternatively a function that | ||||||
| returns serializable data. If you supply a function, it will be passed an object | ||||||
| containing the `org`, `project`, and `release` strings. | ||||||
| First, to identify the source of an error, you must inject metadata that helps identify | ||||||
| which bundles were responsible for the error. You can do this with any of the | ||||||
| Sentry bundler plugins by enabling the `_experiments.moduleMetadata` option. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would love to mention here that this should also work for vite and esbuild. I know that we mention the packages above but I am not sure if people will connect the dots. Doesn't have to be in this PR though!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added, lmk what you think |
||||||
|
|
||||||
| ```javascript | ||||||
| // webpack.config.js | ||||||
|
|
@@ -37,61 +46,68 @@ module.exports = { | |||||
| sentryWebpackPlugin({ | ||||||
| /* Other plugin config */ | ||||||
| _experiments: { | ||||||
| moduleMetadata: ({ org, project, release }) => { | ||||||
| return { team: "frontend", release }; | ||||||
| }, | ||||||
| moduleMetadata: ({ release }) => ({ dsn: "__MODULE_DSN__", release }), | ||||||
| }, | ||||||
| }), | ||||||
| ], | ||||||
| }; | ||||||
| ``` | ||||||
|
|
||||||
| ### `ModuleMetadata` Integration | ||||||
|
|
||||||
| Requires SDK version `7.59.0` or higher. | ||||||
|
|
||||||
| Once metadata has been injected into modules, the `moduleMetadataIntegration` | ||||||
| can be used to look up that metadata and attach it to stack frames with | ||||||
| matching file names. This metadata is then available in other integrations or in | ||||||
| the `beforeSend` callback as the `module_metadata` property on each | ||||||
| `StackFrame`. This can be used to identify which bundles may be responsible | ||||||
| for an error and used to tag or route events. | ||||||
| matching file names. This metadata is then available in the `beforeSend` callback | ||||||
| as the `module_metadata` property on each `StackFrame`. This can be used to identify | ||||||
| which bundles may be responsible for an error and, once the destination is determined, | ||||||
| store it as a list of DSN-release pairs in `event.extra[MULTIPLEXED_TRANSPORT_EXTRA_KEY]` | ||||||
| where multiplexed transport knows to look for. | ||||||
|
maxkosty marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| In practice here is what your Sentry intialization should look like: | ||||||
|
maxkosty marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| ```javascript | ||||||
| import * as Sentry from "@sentry/browser"; | ||||||
| import { init, makeFetchTransport, moduleMetadataIntegration, makeSimpleMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from "@sentry/browser"; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Once we added the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds good.
@lforst We should simplify the API sooner than later though. I feel like current sample code looks convoluted to the point of customers thinking "surely there must be an easier/more elegant way to do it, let me see what those other blog posts that showed up in search say"
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ack, right now it is unfortunately not a priority for us and I'd like to avoid us losing focus but maybe I can sneak your PR in somehow. |
||||||
|
|
||||||
| Sentry.init({ | ||||||
| dsn: "___PUBLIC_DSN___", | ||||||
| integrations: [Sentry.moduleMetadataIntegration()], | ||||||
| init({ | ||||||
| dsn: "__DEFAULT_DSN__", | ||||||
| integrations: [moduleMetadataIntegration()], | ||||||
| makeMultiplexedTransport(makeFetchTransport), | ||||||
| beforeSend: (event) => { | ||||||
| const frames = event?.exception?.values?.[0].stacktrace.frames || []; | ||||||
| // Get all team names in the stack frames | ||||||
| const teams = frames | ||||||
| .filter((frame) => frame.module_metadata && frame.module_metadata.team) | ||||||
| .map((frame) => frame.module_metadata.team); | ||||||
| // If there are teams, add them as extra data to the event | ||||||
| if (teams.length > 0) { | ||||||
| event.extra = { | ||||||
| ...event.extra, | ||||||
| teams, | ||||||
| }; | ||||||
| if (event?.exception?.values?.[0].stacktrace.frames) { | ||||||
| const frames = event.exception.values[0].stacktrace.frames; | ||||||
|
|
||||||
| // Find the last frame with module metadata containing a DSN | ||||||
| const route_to = frames | ||||||
| .filter((frame) => frame.module_metadata && frame.module_metadata.dsn) | ||||||
| .map((v) => v.module_metadata) | ||||||
| .slice(-1); // using top frame only - you may want to customize this according to your needs | ||||||
|
|
||||||
| event.extra = { | ||||||
| ...event.extra, | ||||||
| [MULTIPLEXED_TRANSPORT_EXTRA_KEY]: route_to, | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| return event; | ||||||
| }, | ||||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| Once this is set up, errors - both handled and unhandled - will be automatically routed to the right project. You don't need to do anything special. | ||||||
|
maxkosty marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| ``` | ||||||
| Sentry.captureException(new Error("oh no!")); | ||||||
| // or | ||||||
| throw new Error("oops"); | ||||||
| ``` | ||||||
|
maxkosty marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| ## Routing events to different projects | ||||||
| ## Manually route errors to different projects | ||||||
|
maxkosty marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| Once you've identified which module or modules are likely to be responsible for | ||||||
| an error, you may want to send these events to different Sentry projects. The | ||||||
| multiplexed transport can route events to different Sentry projects based on the | ||||||
| attributes on an event. | ||||||
| If, however, you would like to have more control over the routing of errors to the point | ||||||
| where you explicitly specify the destination for each individual `captureException` that is | ||||||
| possible as well using the more advanced interface multiplexed transport offers. | ||||||
|
maxkosty marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| <Note> | ||||||
| Requires SDK version `7.59.0` or higher. | ||||||
| </Note> | ||||||
|
|
||||||
| The example below uses a `feature` tag to determine which Sentry project to | ||||||
| send the event to. If the event does not have a `feature` tag, we send it to the | ||||||
|
|
@@ -130,107 +146,17 @@ init({ | |||||
| dsn: "__FALLBACK_DSN__", | ||||||
| transport: makeMultiplexedTransport(makeFetchTransport, dsnFromFeature), | ||||||
| }); | ||||||
|
|
||||||
| captureException(new Error("oh no!"), (scope) => { | ||||||
| scope.setTag("feature", "cart"); | ||||||
| return scope; | ||||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| You can then set tags/contexts on events in individual micro-frontends to decide which Sentry project to send the event to. | ||||||
|
|
||||||
| ### `makeMultiplexedTransport` API | ||||||
|
|
||||||
| `makeMultiplexedTransport` takes an instance of a transport (we recommend | ||||||
| `makeFetchTransport`) and a matcher function that returns an array of objects | ||||||
| containing the DSN and optionally the release. | ||||||
|
|
||||||
| ```typescript | ||||||
| interface RouteTo { | ||||||
| dsn: string; | ||||||
| release?: string; | ||||||
| } | ||||||
|
|
||||||
| type Matcher = (param: MatchParam) => RouteTo[]; | ||||||
|
|
||||||
| declare function makeMultiplexedTransport( | ||||||
| transport: (options: TransportOptions) => Transport, | ||||||
| matcher: Matcher | ||||||
| ): (options: TransportOptions) => Transport; | ||||||
| ``` | ||||||
|
|
||||||
| The matcher function runs after all client processing (`beforeSend` option, event processors from integrations). | ||||||
|
|
||||||
| ## Combining `ModuleMetadata` and `makeMultiplexedTransport` | ||||||
|
|
||||||
| `ModuleMetadata` and `makeMultiplexedTransport` can be used together to route | ||||||
| events to different Sentry projects based on the module where the error | ||||||
| occurred. | ||||||
|
|
||||||
| Ensure your modules have injected metadata containing the project DSN and release: | ||||||
| You can then set tags/contexts on events in individual micro-frontends to decide which Sentry project to send the event to as follows: | ||||||
|
|
||||||
| ```javascript | ||||||
| // webpack.config.js | ||||||
| const { sentryWebpackPlugin } = require("@sentry/webpack-plugin"); | ||||||
| <Note> | ||||||
| It is important to always use a local scope when setting the tag (either as shown above or using [withScope](../../enriching-events/scopes/#local-scopes)). Using a global scope e.g. through `Sentry.setTag()` will result in all subsequent events being routed to the same DSN regardless of where they originate. | ||||||
| </Note> | ||||||
|
|
||||||
| module.exports = { | ||||||
| devtool: "source-map", | ||||||
| plugins: [ | ||||||
| sentryWebpackPlugin({ | ||||||
| _experiments: { | ||||||
| moduleMetadata: ({ release }) => ({ dsn: "__MODULE_DSN__", release }), | ||||||
| }, | ||||||
| }), | ||||||
| ], | ||||||
| }; | ||||||
| ``` | ||||||
|
|
||||||
| Then when you initialize Sentry: | ||||||
|
|
||||||
| - Add the `ModuleMetadata` integration so metadata is attached to stack frames | ||||||
| - Add a `beforeSend` callback that sets an `extra` property with the target DSN/release | ||||||
| - Create a transport that routes events when the `extra` property is present | ||||||
|
|
||||||
| ```javascript | ||||||
| import { init, makeFetchTransport, moduleMetadataIntegration, makeMultiplexedTransport } from "@sentry/browser"; | ||||||
|
|
||||||
| const EXTRA_KEY = "ROUTE_TO"; | ||||||
|
|
||||||
| const transport = makeMultiplexedTransport(makeFetchTransport, (args) => { | ||||||
| const event = args.getEvent(); | ||||||
| if ( | ||||||
| event && | ||||||
| event.extra && | ||||||
| EXTRA_KEY in event.extra && | ||||||
| Array.isArray(event.extra[EXTRA_KEY]) | ||||||
| ) { | ||||||
| return event.extra[EXTRA_KEY]; | ||||||
| } | ||||||
| return []; | ||||||
| }); | ||||||
|
|
||||||
| init({ | ||||||
| dsn: "__DEFAULT_DSN__", | ||||||
| integrations: [moduleMetadataIntegration()], | ||||||
| transport, | ||||||
| beforeSend: (event) => { | ||||||
| if (event?.exception?.values?.[0].stacktrace.frames) { | ||||||
| const frames = event.exception.values[0].stacktrace.frames; | ||||||
| // Find the last frame with module metadata containing a DSN | ||||||
| const routeTo = frames | ||||||
| .filter((frame) => frame.module_metadata && frame.module_metadata.dsn) | ||||||
| .map((v) => v.module_metadata) | ||||||
| .slice(-1); // using top frame only - you may want to customize this according to your needs | ||||||
|
|
||||||
| if (routeTo.length) { | ||||||
| event.extra = { | ||||||
| ...event.extra, | ||||||
| [EXTRA_KEY]: routeTo, | ||||||
| }; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return event; | ||||||
| }, | ||||||
| captureException(new Error("oh no!"), (scope) => { | ||||||
| scope.setTag("feature", "cart"); | ||||||
| return scope; | ||||||
| }); | ||||||
| ``` | ||||||
Uh oh!
There was an error while loading. Please reload this page.