Observability Plugin

The Observability Plugin makes Module Federation loading observable. It records runtime loading events, summarizes the final result, prints a stable traceId when loading fails, and can correlate runtime failures with build information.

The plugin is designed for Module Federation 2.5.0 and later. If a project is on an older MF version, runtime error codes still help with basic troubleshooting, but the richer report and loading observability workflow requires upgrading to 2.5.0+ and enabling this plugin.

Use it when you want to answer questions such as:

  • Did this remote load successfully?
  • Which phase failed: manifest, remoteEntry, init, expose, factory, or shared?
  • Did the load recover through a runtime fallback or recovery path?
  • Which shared dependency provider and version were selected?
  • Did preloadRemote actually finish loading its resources?
  • What report should I give to a human or AI coding agent?

Shared observability is scoped to the MF instance. It tells you which MF instance loaded a shared dependency, which registered provider/version was selected, and the related scope, version, and eager configuration. It does not guarantee a causal link from that shared dependency back to a specific remote/expose. When a flow involves multiple shared dependencies, inspect all phase: "shared" events. summary.shared is only the last observed shared summary.

If the build plugin supplies customShareInfo but no registered shared provider matches it, this is not always a fatal error. The report describes the handled path as summary.outcome: "recovered", summary.phases.shared.status: "complete", and shared.reason: "custom-share-info-unmatched". It means the runtime continued through a recoverable path. Inspect shared configuration only if you expected a specific provider/version to be selected.

Preload observability answers whether preloadRemote actually finished loading its resources. After preloadRemote completes, reports include phase: "preload" resource results with the resource URL, resource type, status, and preload id. Status can be success, error, timeout, or cached. When the call does not specify exposes, the id is remoteName/*. When exposes are provided, each expose is recorded separately as remoteName/expose.

If you want to try the report workflow first, or inspect and export reports directly from the page, install the latest Module Federation Chrome extension. The Loading Trace tab reads reports from the page's own observability plugin. If the page has not installed the plugin, the extension can also start temporary collection for the current tab. See Chrome Devtool Loading Trace for the full workflow.

Install

npm install @module-federation/observability-plugin

Browser Runtime

Use the default entry in browser runtime code.

mf-runtime.ts
import { createInstance } from '@module-federation/runtime';
import { ObservabilityPlugin } from '@module-federation/observability-plugin';

export const mf = createInstance({
  name: 'runtime_host',
  remotes: [
    {
      name: 'remote1',
      entry: 'https://example.com/mf-manifest.json',
    },
  ],
  plugins: [
    ObservabilityPlugin({
      level: 'verbose',
      browser: {
        enabled: true,
        scope: 'runtime_host',
      },
    }),
  ],
});

When a Module Federation load fails, the plugin prints a compact console.error hint:

[Module Federation] Observability report generated
traceId: mf-...
phase: manifest
errorCode: RUNTIME-003
read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...")

Run the read: command in the browser console to get the full report.

You can also read reports directly:

window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getLatestReport();
window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getReport('mf-...');
window.__FEDERATION__.__OBSERVABILITY__.runtime_host.getReports({ limit: 5 });
window.__FEDERATION__.__OBSERVABILITY__.runtime_host.findReports({
  remote: 'remote1',
});
window.__FEDERATION__.__OBSERVABILITY__.runtime_host.exportReport('mf-...');

If you want to observe loading chains in development, or if the page stays in a loading state before any error is printed, enable the browser reader. Development browser mode prints start logs by default:

ObservabilityPlugin({
  level: 'verbose',
  browser: {
    enabled: true,
    scope: 'runtime_host',
  },
});

The plugin prints console.info only when loadRemote or loadShare starts. The line includes the traceId and read command. Agents can use that traceId to inspect the current report, including status: "pending", summary.phases, updatedAt, and duration. Set trace.printStart: false to disable it in development browser mode. In production browser mode, start logs are disabled by default and require trace.printStart: true.

Production Runtime

In production, avoid exposing a public browser reader by default. Keep console output small and upload reports through your own system.

mf-runtime.ts
import { ObservabilityPlugin } from '@module-federation/observability-plugin';

export const observabilityPlugin = ObservabilityPlugin({
  level: 'summary',
  browser: {
    mode: 'production',
  },
  onReport(report) {
    if (report.status === 'error' || report.summary.outcome === 'recovered') {
      navigator.sendBeacon(
        '/api/mf-observability',
        JSON.stringify({
          traceId: report.traceId,
          status: report.status,
          diagnosis: report.diagnosis,
          summary: report.summary,
          remote: report.remote,
          shared: report.shared,
          moduleInfo: report.moduleInfo,
        }),
      );
    }
  },
});

In production browser mode, the console hint only contains traceId and known errorCode. The full report should come from onReport, exportReport(), or your own telemetry backend.

Analyze Reports from onReport

onReport is called whenever a report is updated. Production apps usually do not need to store every successful report, but this callback is the right place to upload failures, recovered paths, or selected successful loading chains to your own system.

Common strategies:

  • Troubleshooting only: upload report.status === "error" and report.summary.outcome === "recovered".
  • Shared dependency auditing: also upload report.summary.outcome === "shared-resolved" so you can see which provider and version were selected.
  • Preload result auditing: also upload report.summary.outcome === "preloaded" or failed phase: "preload" events to count successful, failed, timed out, and cached preload resources.
  • Full loading observability: sample successful runtime-loaded, component-loaded, and shared-resolved reports.

After you have a report, read it in this order:

  1. diagnosis: owner hint, key facts, and suggested next actions.
  2. summary: final result. runtime-loaded means the remote loaded, component-loaded means business code reported readiness, shared-resolved means a shared provider/version was selected, preloaded means preload resources completed, failed means loading failed, and recovered means the runtime continued through a recoverable path.
  3. remote / shared: the current load target. For shared reports, inspect provider, requiredVersion, selectedVersion, and availableVersions.
  4. moduleInfo: deployment-provided module information, useful for snapshot matching issues.
  5. events: the ordered timeline, useful for finding the phase that got stuck.

Example:

ObservabilityPlugin({
  level: 'summary',
  browser: {
    mode: 'production',
  },
  onReport(report) {
    const outcome = report.summary.outcome;
    const shouldUpload =
      report.status === 'error' ||
      outcome === 'recovered' ||
      outcome === 'shared-resolved' ||
      outcome === 'preloaded';

    if (!shouldUpload) {
      return;
    }

    navigator.sendBeacon(
      '/api/mf-observability',
      JSON.stringify({
        traceId: report.traceId,
        status: report.status,
        outcome,
        diagnosis: report.diagnosis,
        summary: report.summary,
        remote: report.remote,
        shared: report.shared,
        moduleInfo: report.moduleInfo,
        events: report.events,
      }),
    );
  },
});

You can give the uploaded report to an AI coding agent:

/mf observability

Here is an MF observability report uploaded from production.
Please tell me whether the load succeeded, where it failed, who likely owns the issue, and how to fix it.

<paste report JSON>

Runtime Options

ObservabilityPlugin(options) supports these runtime options:

OptionTypeDefaultDescription
enabledbooleantrueEnable or disable the plugin. Disabled plugins do not record events or generate reports.
level'summary' | 'verbose''summary'Report detail level. verbose keeps the full event timeline.
maxEventsnumberbuilt-in limitMaximum retained events for one plugin instance.
consolebooleantruePrint a short console.error hint when a load fails.
printRawStackbooleanfalsePrint the raw error stack to console. Keep this off by default in production.
stackTrace.enabledbooleantrueStore a clipped stack in the report.
stackTrace.maxLinesnumberbuilt-in limitMaximum stack lines kept in the report.
stackTrace.maxLengthnumberbuilt-in limitMaximum stack characters kept in the report.
collectorboolean | { enabled?: boolean; port?: number }falsePOST browser runtime events to a local collector. true uses 127.0.0.1:17891; custom config only needs a port. This is for local AI debugging, and the runtime plugin does not start the server.
browser.enabledbooleanfalseExpose the browser reader on window.__FEDERATION__.__OBSERVABILITY__.
browser.scopestringhost nameBrowser reader namespace, for example runtime_host.
browser.mode'development' | 'production''development'Browser output mode. Production mode keeps console hints minimal.
trace.printStartbooleantrue in development browser mode, false in production browser modePrint console.info when loadRemote or loadShare starts so development agents can get the traceId. Production mode requires an explicit true.
react.enabledbooleantrueMaster switch for React-specific debugging options. Set to false to disable all React wrapping.
react.injectLoadedCallbackbooleanfalseExplicitly wrap matched remote React components and inject onMFRemoteLoaded. This changes the component reference; use it as a temporary debugging option and remove it after the issue is fixed.
react.remoteIdsstring[][]Limit callback injection to specific remote requests, such as remote/Button or ./Button. Empty means no remote filter.
react.defaultExportMode'preserve' | 'component'autoControls whether { default: Component } remotes return the wrapped component directly. Usually keep the default.
onEvent(event, report, context) => voidundefinedCalled whenever an observability event is recorded.
onReport(report, context) => voidundefinedCalled whenever a report is updated. Production apps commonly upload reports here.
onRawError(error, context) => voidundefinedCalled with the original error object for integration with your own error system.

Node or SSR Runtime

Use the Node entry when you want local report files.

mf-node-runtime.ts
import { createInstance } from '@module-federation/runtime';
import { ObservabilityPlugin } from '@module-federation/observability-plugin/node';

createInstance({
  name: 'node_host',
  remotes: [],
  plugins: [
    ObservabilityPlugin({
      level: 'verbose',
      fileOutput: true,
      directory: '.mf/observability',
    }),
  ],
});

The Node entry writes:

  • .mf/observability/latest.json: latest complete report
  • .mf/observability/events.jsonl: event stream for multiple traces

Read latest.json first. Use events.jsonl only when you need ordering or multiple traces.

Build Observability

Add the build plugin next to your Module Federation build plugin when you want separate build-side evidence.

webpack.config.js
const {
  ModuleFederationPlugin,
} = require('@module-federation/enhanced/webpack');
const {
  ObservabilityBuildPlugin,
} = require('@module-federation/observability-plugin/build');

const moduleFederationOptions = {
  name: 'runtime_host',
  remotes: {
    remote1: 'remote1@https://example.com/mf-manifest.json',
  },
  exposes: {
    './Button': './src/Button',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.0.0' },
  },
};

module.exports = {
  plugins: [
    new ModuleFederationPlugin(moduleFederationOptions),
    new ObservabilityBuildPlugin({
      moduleFederation: moduleFederationOptions,
    }),
  ],
};

Build observability can write:

  • .mf/observability/build-info.json
  • .mf/observability/build-report.json

Runtime reports do not embed these files. When debugging needs build evidence, read the build file separately and compare it with the runtime report.

Mark Business Success

Module Federation can know that a remote module loaded. It cannot always know that your business component finished its own data loading, chart rendering, or SDK initialization.

When react.injectLoadedCallback is explicitly enabled, the plugin injects an onMFRemoteLoaded prop into matched remote React components. The producer can call it when its own ready condition is met:

import { useEffect } from 'react';
import type { OnMFRemoteLoaded } from '@module-federation/observability-plugin';

export default function RemotePanel({
  onMFRemoteLoaded,
}: {
  onMFRemoteLoaded?: OnMFRemoteLoaded;
}) {
  useEffect(() => {
    onMFRemoteLoaded?.({
      metadata: {
        dataReady: true,
      },
    });
  }, [onMFRemoteLoaded]);

  return <section>Remote panel</section>;
}

Consumer-side code can still call the instance method directly when needed:

import { getInstance } from '@module-federation/runtime';
import '@module-federation/observability-plugin';

getInstance()?.markComponentLoaded({
  requestId: 'remote1/Button',
  componentName: 'Button',
  metadata: {
    route: '/dashboard',
  },
});

The report will include component:business-loaded and summary.outcome: "component-loaded".

Inject React Loaded Callback

For development, AI debugging, or temporary production debugging, you can explicitly inject a loaded callback into matched remote React components:

ObservabilityPlugin({
  level: 'verbose',
  react: {
    injectLoadedCallback: true,
    remoteIds: ['remote/Button'],
  },
});

When enabled, the plugin tries to detect remote React function components after loadRemote succeeds and wraps them with a component that does not add DOM nodes. The wrapper injects only the onMFRemoteLoaded prop. It does not observe React mount, render lifecycle, or timeout. When the producer calls props.onMFRemoteLoaded?.(), the report records component:business-loaded.

If summary.componentLoaded is false after enabling react.injectLoadedCallback, first check whether the producer source actually calls props.onMFRemoteLoaded?.(...). If it does not, the report can only prove that the remote resource loaded; it cannot prove whether the component reached the producer's business-ready point. If the producer source is unavailable, ask the producer owner to confirm whether the callback was added.

This option changes the component reference because loadRemote returns a wrapper component. Use remoteIds to keep the matched scope narrow, and remove this option after the production issue is fixed.

Use With the mf Skill

Install the skill:

npx skills add module-federation/agent-skills --skill mf -y

When the console prints an observability hint, ask your agent:

/mf observability
I saw this Module Federation console error:

[Module Federation] Observability report generated
traceId: mf-...
read: window.__FEDERATION__.__OBSERVABILITY__["runtime_host"].getReport("mf-...")

Please read the report and fix the issue.

If you are in Node or SSR, give the agent the file path instead:

/mf observability
Read .mf/observability/latest.json and explain the likely owner and fix.

If your production app uploads reports, give the uploaded report or traceId to the agent:

/mf observability
Here is the uploaded report for traceId mf-...
Tell me whether this is a host, remote, shared, network, or build issue.

What the AI Reads First

The skill reads fields in this order:

  1. diagnosis
  2. summary
  3. moduleInfo
  4. events

If build-side evidence is needed, read .mf/observability/build-info.json or .mf/observability/build-report.json as a separate file.

Reports omit undefined fields. If a field is absent, treat it as not observed or not relevant for this trace.