Skip to content
Merged
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,29 @@ 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
Below we offer two approaches. Please note that `Sentry.init()` must be called only once, doing otherwise will result in undefined behavior.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very good to mention. I wonder if we should highlight this even stronger.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


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.
## Automatically Route Errors to Different Projects Depending on Module

`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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, lmk what you think


```javascript
// webpack.config.js
Expand All @@ -37,61 +48,63 @@ 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. Once the destination is determined, you can
store it as a list of DSN-release pairs in `event.extra[MULTIPLEXED_TRANSPORT_EXTRA_KEY]`
for the multiplexed transport to reference for routing.

In practice, here is what your Sentry initialization should look like:

```javascript
import * as Sentry from "@sentry/browser";
import { init, makeFetchTransport, moduleMetadataIntegration, makeSimpleMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from "@sentry/browser";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { init, makeFetchTransport, moduleMetadataIntegration, makeSimpleMultiplexedTransport, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from "@sentry/browser";
import { init, makeFetchTransport, moduleMetadataIntegration, MULTIPLEXED_TRANSPORT_EXTRA_KEY } from "@sentry/browser";

Once we added the makeSimpleMultiplexedTransport API we can revisit this but for now I'd leave it away.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good.
Reverting sample code to original including this change (as it hasn't been tested)

  • [Automatic] remove redundant if (routeTo.length) { check for empty array (done in matcher)

@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"

Copy link
Contributor

Choose a reason for hiding this comment

The 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) {
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,
teams,
[MULTIPLEXED_TRANSPORT_EXTRA_KEY]: route_to,
};
}

return event;
},
});

Sentry.captureException(new Error("oh no!"));
```

## Routing events to different projects
Once this is set up, errors - both handled and unhandled - will be automatically routed to the right project.

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.

## Manually Route Errors to Different Projects

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`,
you can do that with the more advanced interface multiplexed transport offers.

<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
Expand Down Expand Up @@ -130,107 +143,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
You can then set tags/contexts on events in individual micro-frontends to decide which Sentry project to send the event to as follows:

`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.
<Note>
It is important to always use a local scope when setting the tag (either as shown below 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>

```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:

```javascript
// webpack.config.js
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");

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;
});
```