@module-federation/observability-plugin
v2.6.0
Published
Runtime observability plugin for Module Federation loading flows.
Readme
@module-federation/observability-plugin
Runtime observability plugin for Module Federation loading flows.
This package is designed for Module Federation 2.5.0 and later. Older
projects can still use runtime error codes, but the full observability workflow
requires upgrading the MF runtime and installing this plugin.
This package is currently the minimal observability foundation. It records structured in-memory loading events when the plugin is installed and not disabled. Optional outputs are conservative by default: no browser global, no Node file output, and no raw error stack in console output.
import { createInstance } from '@module-federation/runtime';
import { ObservabilityPlugin } from '@module-federation/observability-plugin';
createInstance({
name: 'host',
plugins: [
ObservabilityPlugin({
level: 'verbose',
browser: {
enabled: true,
scope: 'host',
},
}),
],
remotes: [],
});The plugin does not upload data or expose a browser global by default. Reports
are kept in memory. Runtime request URLs, error messages, and stored error
stacks keep their original query/hash and error details because those values are
often needed for debugging. Large deployment locator fields such as
publicPath and remoteEntry are only length-limited.
Safe Observability
Enable only the output channels that match the environment:
- Browser dev: use
browser.enabled: truewith a scoped reader such aswindow.__FEDERATION__.__OBSERVABILITY__.host. - Chrome DevTools integration: use
devtools: truetogether with browser output so a browser extension can receive loading events throughwindow.postMessage. - Agent-led browser dev: use
collector: trueto POST reports to the local skill collector on127.0.0.1:17891. - Browser prod: set
browser.mode: "production"so console output stays limited totraceIdand knownerrorCode; export full reports only through explicit app-owned flows such asexportReport()oronReport. - Node / SSR: use the Node entry
@module-federation/observability-plugin/nodeand setfileOutput: truewhen local files are needed. - Build: use
ObservabilityBuildPluginwhen build-side files are needed for later comparison.
Reports include the loading timeline, selected host/remote/shared facts,
original runtime URLs, original error message/stack, clipped deployment
moduleInfo when it is relevant, and user-provided observability
metadata with count and length limits. Reports do not collect request headers,
cookies, authorization values, remote response bodies, remote source, module
source, React props, full moduleInfo.modules, full moduleInfo.shared, or
asset lists from deployment moduleInfo.
Fields whose value is undefined are omitted from returned reports and events,
so missing fields should be read as "not observed or not relevant" instead of a
literal value.
Failure hints are printed with console.error so browser DevTools, CDP-based
agents, Node logs, and log collection systems can detect that a Module
Federation load failed and then use the printed traceId to fetch the full
report. Successful or recovered reports are still available through the reader
APIs and callbacks, but they are not promoted to console errors. When the
browser reader is enabled in development mode, the plugin prints a small
console.info line by default when a loadRemote or loadShare trace starts.
The line includes the traceId and read command so an agent can inspect pending
loading state before a timeout or error happens. In production browser mode,
start logs are disabled by default; set trace.printStart: true only when you
explicitly want them.
The runtime only exposes the loading lifecycle hooks needed to know whether the
main flow started, succeeded, or failed. This plugin listens to those hooks,
derives detailed reasons like shared version mismatch or eager boundary issues,
and exposes the final loading state through a small summary object:
Runtime Core deliberately keeps this boundary narrow. It registers and invokes the lifecycle hooks with raw facts from the real execution path. Candidate lists, shared selection and registration reasons, resource and Bridge timing, error classification, correlation, and sanitization are all derived here in the observability plugin. Applications that do not install this plugin do not pay for report construction or keep observability state in Runtime Core.
runtime-loaded: Module Federation finished loading the remote module.component-loaded: business code calledmarkComponentLoaded, or a producer called the injectedonMFRemoteLoadedcallback.preloaded:preloadRemotefinished loading the selected resources.failed: the load failed andfailedPhasepoints to the first specific failing phase.recovered: loading hit an error but a fallback/recovery path returned a result. For shared loading, thecustom-share-info-unmatchedreason means build-timecustomShareInfodid not match a registered provider, but the runtime handled it as a non-fatal result instead of a loading failure.
summary.componentLoaded: false only means no component-level ready signal was
observed. If react.injectLoadedCallback: true is enabled but this field is
still false, check whether the producer actually calls
props.onMFRemoteLoaded?.(...). Without that producer call, the report can only
confirm that the remote resource loaded; it cannot prove whether the React
component reached the producer's business-ready point.
For remote loading, the plugin listens to runtime lifecycle hooks such as
beforeRequest, afterMatchRemote, onLoad, afterLoadRemote,
errorLoadRemote, loadEntry, afterLoadEntry, beforeInitRemote,
afterInitRemote, beforeGetExpose, afterGetExpose,
beforeExecuteFactory, afterExecuteFactory, and snapshot resolve hooks. It
does not return a value from observer hooks, so it does not change fallback or
retry results from plugins such as @module-federation/retry-plugin.
Successful verbose reports include the key runtime stages: remote match,
manifest, remoteEntry load, remoteEntry init, expose resolution, module factory
execution, and final load completion. When a stage has matching start and end
events, the end event includes a bounded duration value.
Preload reports include resource-level results from preloadRemote. Each
result records the resource URL, resourceType, initiator, preload id, and
status: success, error, timeout, or cached. Calls without exposes use
remoteName/* as the preload id. Calls with exposes are recorded per
expose as remoteName/expose. The plugin does not change the existing
generatePreloadAssets return shape; preload resources are still plain URL
arrays.
Runtime resource hooks also receive a resourceContext object on manifest,
remoteEntry, preload JS, and preload CSS resource loads. It contains
initiator, id, resourceType, url, and expose when available, so custom
loaders can tell whether the resource was requested by loadRemote,
preloadRemote, or loadShare without parsing the URL.
The report also keeps compact loading state under summary. It contains the
final outcome, per-phase status and duration under summary.phases, safe
cache/recovery markers under summary.flags, and the last resolved shared
provider/version under summary.shared when a shared dependency was observed.
level: "summary" omits start events from the stored timeline but still keeps
the derived durations on the matching success/error events. level: "verbose"
keeps the full timeline.
Each report also includes a deterministic diagnosis object. It is generated
by engineering rules, not by an AI model. It keeps the final outcome, likely
owner, completed and pending phases, observability facts, documentation link when
a known runtime error code is present, and a short list of next checks. This is
the field a person or AI coding agent should read first before falling back to
the raw events timeline.
For agent-led debugging, use the repository's single mf skill entry with the
observability sub-command. The skill is the maintained guide for reading
reports and deciding the next debugging step.
Divebell targets
When divebell is enabled, the observability plugin syncs Module Federation
loading state into Divebell targets. The host application does not need to
import Divebell directly.
ObservabilityPlugin({
level: 'verbose',
divebell: {
source: 'module-federation',
},
});divebell: true uses the Divebell runtime found on window, or creates and
installs one when needed. The object form also accepts enabled, source, a
custom runtime, or a custom browser host. Set enabled: false to keep a
shared configuration object while disabling synchronization.
Use the Divebell CLI to inspect the page:
divebell targets --url "http://127.0.0.1:3005/observability"
divebell snapshot --url "http://127.0.0.1:3005/observability"mf:remote:<remoteName>
Type: mf.remote
This target represents the remote instance as a whole. Use it to understand whether a remote is currently known, loading, ready, failed, or recovered. Do not use the remote target to prove that a specific exposed module is ready; use the expose target for that.
Statuses:
registered: the remote is known, but no clear loading result has been observed yet.loading: a remote lifecycle phase is currently in progress.ready: the latest relevant remote lifecycle phase completed successfully.error: a remote lifecycle phase failed and no recovery result was observed.recovered: loading hit an error path, but Module Federation returned a recovery result.
Snapshot data:
instanceRef: stable observability reference for the latest MF instance.hostName: array of host or consumer names observed for this remote.runtimeVersion: Module Federation runtime version from the latest report.remote: remote definition fields such asname,alias,entry,entryGlobalName, andtype.exposes: array of{ targetId }entries. Each target id points to amf:remote:<remoteName>:expose:<exposeName>target.reportCount: number of related reports used to build this remote snapshot.
The plugin does not create separate mf:manifest:*, mf:remote-entry:*, or
mf:consumer:* targets. Manifest and remoteEntry details are kept as phases on
the remote or expose data.
Common wait:
divebell wait-for mf:remote:runtime_remote2 ready --url "http://127.0.0.1:3005/observability"mf:remote:<remoteName>:expose:<exposeName>
Type: mf.remote.expose
This target represents one exposed module request from a remote. Use it when an agent needs to wait until a specific remote module is usable.
Statuses are the same as mf.remote:
registeredloadingreadyerrorrecovered
Snapshot data:
instanceRef: stable observability reference for the MF instance.traceId: report id for the latest matching trace.requestId: requested remote module id, for exampleruntime_remote2/ButtonOldAnt.requestAlias: request alias when Module Federation provides one.hostName: array of host or consumer names observed for this expose.runtimeVersion: Module Federation runtime version from the latest report.consumers: consumer names that loaded this expose.lastPhase: latest phase name from the report summary.phases: phase summary, including entries such asmatchRemote,manifest,remoteEntry,remoteEntryInit,expose, andloadRemote.loadedBefore: cached producer/consumer information when the runtime reports that this module was already loaded.
The expose target does not repeat remote, expose, status, state,
outcome, or currentPhase inside data. The target id and outer target
status already carry that information.
Common wait:
divebell wait-for mf:remote:runtime_remote2:expose:ButtonOldAnt ready --url "http://127.0.0.1:3005/observability"mf:shared:<sharedName>:<version>:<shareScope>
Type: mf.shared
This target represents one observed shared dependency entry. The id includes the shared package name, selected or requested version, and share scope. The plugin only registers shared targets that have been observed by the runtime.
Statuses:
unloaded: the shared dependency was observed, but no active loading or loaded signal was seen.loading: shared loading has started or the shared entry is currently loading.loaded: the shared entry was loaded or resolved successfully.recovered: shared loading hit a handled fallback path, such as a bundler-runtime provided shared entry.error: shared loading failed.
Snapshot data:
instanceRef: stable observability reference for the MF instance.traceId: report id for the latest matching trace.requestId: shared request id.hostName: Module Federation host name that resolved the shared dependency.runtimeVersion: Module Federation runtime version.shared.name: shared package name.shared.shareScope: share scope array.shared.version: version used in the target id.shared.requiredVersion: requested version range when available.shared.provider: selected provider when available.shared.singleton,shared.strictVersion,shared.eager,shared.strategy: shared config details when available.shared.loaded: whether the shared entry is loaded.shared.loading: included only while the shared entry is loading. It is omitted onceloadedis true.shared.reason: reason such assync-async-boundaryorcustom-share-info-unmatched.shared.definedBy:bundler-runtimewhen the runtime reports a handled bundler-runtime shared path.lastPhase: latest phase name from the report summary.phases: phase summary for shared loading.
The shared snapshot intentionally omits selectedVersion,
availableVersions, get, useIn, and from. If a user needs broader
runtime global state, use the Divebell actions below.
Common waits:
divebell wait-for mf:shared:observability-provider-choice:2.0.0:observability-provider-scope loaded --url "http://127.0.0.1:3005/observability"
divebell wait-for mf:shared:observability-async-shared:1.0.0:default error --url "http://127.0.0.1:3005/observability"mf:shared-conflict:<sharedName>:<shareScope>
Type: mf.shared.conflict
This target is created when the plugin observes multiple versions of a
singleton shared dependency. Its status is warning. Snapshot data includes
the stable instanceRef, traceId, shared name and scope, current provider and
version, all observed versions, and the compact shared snapshot. Use the normal
mf:shared:* target to inspect loading state; use this conflict target to
diagnose singleton version drift.
Actions
The plugin also registers safe Divebell actions for deeper inspection:
mf:list-reports: list report summaries. Supports filters such asremote,expose,shared,traceId,instanceRef,status, andoutcome.mf:get-runtime-state: read the current safe multi-instance runtime state.mf:get-latest-report: read the latest report.mf:get-report: read one report bytraceId.mf:export-report: export a report, defaulting to the latest report.mf:get-federation-global: read a compact summary of the current__FEDERATION__or__VMOK__global.mf:get-federation-module-info: readmoduleInfoor onemoduleInfoentry.mf:list-federation-instances: list current runtime instances.mf:get-federation-instance-config: read one runtime instance config.
Example:
divebell run-action mf:list-reports --url "http://127.0.0.1:3005/observability" --payload '{"remote":"runtime_remote2"}'Multi-instance runtime state
The browser reader and mf:get-runtime-state action return the same safe state
model:
window.__FEDERATION__.__OBSERVABILITY__.host.getRuntimeState();The result includes stable instanceRef values, instance roles and evidence,
known remotes and loaded producers, share-scope summaries, Bridge state,
consumer-to-producer relationships, deployment moduleInfo, and capability
completeness. Use instanceRef when filtering reports or selecting an instance;
array indexes are only a compatibility fallback and are not stable identifiers.
errorLoadShare is used only for observation. Shared dependency miss, version
mismatch, and eager boundary errors are not retried by the retry plugin by
default because they are usually configuration or availability problems instead
of transient network failures. When a build plugin supplies customShareInfo
and the runtime reports a handled miss, the observability report uses a
recovered outcome instead of marking the trace as failed.
The Chrome extension entry skips shared events for older or preview runtime
versions because those runtimes do not expose the same shared lifecycle
contract.
Business code can mark its own success condition with a fixed event. When React
callback injection is explicitly enabled, the wrapper injects an
onMFRemoteLoaded prop into the remote component. The producer can call it when
the component's 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>;
}If the app wants to mark readiness from the consumer side, it can still call the instance method directly:
import { getInstance } from '@module-federation/runtime';
import '@module-federation/observability-plugin';
getInstance()?.markComponentLoaded({
requestId: 'remote/Button',
componentName: 'Button',
metadata: {
route: '/settings',
},
});Both paths record component:business-loaded on the same trace when possible.
Business metadata is optional. String values keep their original details and are
only length-limited, so user-provided metadata remains trustworthy. The instance
method is attached when the observability plugin is registered. If an
application uses multiple runtime instances, call it on the instance that
registered this plugin.
React callback injection is available only when explicitly enabled:
ObservabilityPlugin({
level: 'verbose',
react: {
injectLoadedCallback: true,
remoteIds: ['remote/Button'],
},
});When this option is enabled, the plugin tries to wrap remote function components
returned by loadRemote. The wrapper does not add DOM nodes. It injects the
onMFRemoteLoaded prop only. It does not observe React mount, render lifecycle,
or timeout. When the producer calls the callback, the report records
component:business-loaded. This option changes the component reference because
it returns a wrapper component, so use it as a temporary debugging switch and
remove it after the production issue is fixed.
If summary.componentLoaded is still false after enabling this option, inspect
the producer first. If the producer has not called onMFRemoteLoaded, the report
only proves remote runtime loading, not component business readiness. If the
producer source is unavailable, ask the producer owner to confirm whether the
callback was added.
Use react.remoteIds to limit this behavior to the remote requests you are
actively debugging. If remoteIds is empty, the plugin wraps detected React
function components loaded by the runtime instance that registered it.
Browser output is available only when the plugin option explicitly enables it. When browser output is enabled, the report can be read from:
window.__FEDERATION__.__OBSERVABILITY__.host.getLatestReport();
window.__FEDERATION__.__OBSERVABILITY__.host.getReport('mf-trace-id');
window.__FEDERATION__.__OBSERVABILITY__.host.getReports({ limit: 5 });
window.__FEDERATION__.__OBSERVABILITY__.host.findReports({ remote: 'remote1' });
window.__FEDERATION__.__OBSERVABILITY__.host.exportReport('mf-trace-id');Chrome DevTools panels can opt in to event delivery without polling the page:
ObservabilityPlugin({
level: 'verbose',
browser: {
enabled: true,
scope: 'host',
mode: 'development',
},
trace: {
printStart: true,
},
devtools: true,
});This posts structured event/report snapshots to the page with
window.postMessage. Browser extensions can forward those messages from their
content script to the panel. The channel is disabled by default.
getReports({ limit }) returns recent reports newest first. findReports() can
filter by traceId, remote, expose, shared, status, or outcome.
exportReport() returns a copied report object, using the latest report when no
traceId is provided.
For agent-led development debugging where a page may stay in a loading state, enable the browser reader. Development browser mode prints start traces by default:
ObservabilityPlugin({
level: 'verbose',
browser: {
enabled: true,
scope: 'host',
},
});This prints only loadRemote and loadShare start lines. It does not print a
line for every internal phase. Set trace.printStart: false to disable it in
development browser mode. In production browser mode, set
trace.printStart: true to opt in.
If the agent cannot execute JavaScript in the browser page, enable the local collector and start the collector from the MF skill:
ObservabilityPlugin({
level: 'verbose',
collector: true,
});collector: true posts event/report snapshots to:
http://127.0.0.1:17891/__mf_observabilityUse a custom local port only when the default port is occupied:
ObservabilityPlugin({
collector: {
enabled: true,
port: 17892,
},
});The runtime plugin does not create a server. The MF skill starts a temporary
local Node collector, writes reports under .mf/observability/collector, and
the agent reads those files. The collector path is local-only and does not
execute code or control the page. Collector delivery is controlled by the
collector option; debug mode only decides whether a failed collector request
prints a debug log.
For browser production use, set browser.mode: "production" when the runtime
console must stay minimal:
ObservabilityPlugin({
browser: {
enabled: true,
scope: 'host',
mode: 'production',
},
});In production browser mode, the console.error hint only includes the traceId
and known errorCode. It does not print the report body, raw stack, request
URL, or read: command. Full reports are still available only through explicit
user choices such as exportReport() or an application-owned onReport upload.
Production applications that want richer observability should prefer
onReport / onEvent to forward reports to their own telemetry system instead
of exposing a public browser global.
Node file output is provided by the Node-specific entry:
import { createInstance } from '@module-federation/runtime';
import { ObservabilityPlugin } from '@module-federation/observability-plugin/node';
createInstance({
name: 'host',
plugins: [
ObservabilityPlugin({
level: 'verbose',
fileOutput: true,
directory: '.mf/observability',
}),
],
remotes: [],
});When Node file output is explicitly enabled, the Node entry writes:
.mf/observability/latest.json: a formatted copy of the latest complete report, includingtraceId, top-level status/error fields,diagnosis,summary, clippedmoduleInfowhen relevant, and the report's ownevents..mf/observability/events.jsonl: append-only event stream. Each line is one JSON object for one runtime event and includes fields such astraceId,timestamp,phase,status, remote/shared/expose context, and error fields when present.
Read latest.json first. Use events.jsonl only when multiple traces must be
compared or when the full event ordering for a traceId is needed.
On errors, the plugin prints a small console hint with the traceId and the
available read path. The console hint is intentionally small and does not carry
the full report or the raw stack by default. If a user explicitly needs the
full stack, they can opt in with printRawStack: true or capture the original
error through onRawError. The default browser/runtime entry does not include
Node file output code.
Build-time observability is provided by the build-specific entry:
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
const { ObservabilityBuildPlugin } = require('@module-federation/observability-plugin/build');
const moduleFederationOptions = {
name: 'host',
remotes: {
remote1: 'remote1@http://localhost:3001/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,
}),
],
};When this optional build plugin is installed, it writes
.mf/observability/build-info.json. The file is a summary of the Module
Federation build configuration and generated manifest/stats facts:
bundler name/version, Module Federation plugin version when available, build
version when available, remoteEntry file/type/publicPath mode, remotes,
exposes, and shared dependencies. It intentionally omits local expose source
paths, asset lists, source code, and environment variables. Remote URLs and the
remoteEntry.publicPath deployment locator keep query/hash data. If build
observability output fails, the build continues and a bundler warning is emitted.
When the build has compilation errors, or when the observability plugin cannot
write its own build output, the build plugin writes
.mf/observability/build-report.json. The report has the same high-level shape
as runtime reports: a traceId, status, failedPhase, events,
summary.error, diagnosis, and the top-level build object. Clean builds
remove the stale report file so readers do not mistake an old build error for
the current state.
Runtime reports do not include build facts. summary is only the loading-state
view. If debugging needs build-side evidence, read
.mf/observability/build-info.json or .mf/observability/build-report.json
separately and compare it with the runtime report.
For snapshot-dependent failures, such as RUNTIME-007, reports can also include
top-level moduleInfo. This field is collected only for failures that depend on
__FEDERATION__.moduleInfo. It is clipped by default: the plugin keeps matching
entries with only name, publicPath, getPublicPath, remoteEntry, and
globalName, keeps the deployment locator fields only length-limited, and
removes large fields such as modules and shared.
Runtime errors are normalized into stable fields on both events and reports:
errorCode: for exampleRUNTIME-003orRUNTIME-008when the original error includes one.failedPhaseandlifecycle: where the failure happened.ownerHint: a deterministic hint such ashost,remote,shared, ornetwork.retryable: whether the observed failure looks transient.errorContext: a context object with values such as manifest URL, remote name,entryGlobalName, request id, expose, or shared package.
The first batch includes specific diagnosis facts and actions for
RUNTIME-001, RUNTIME-003, RUNTIME-004, RUNTIME-005, RUNTIME-006, and
RUNTIME-008. RUNTIME-008 is further classified as network, timeout,
script-execution, or unknown in errorContext.resourceErrorType and
diagnosis.facts.resourceErrorType.
Shared dependency reports include only evidence fields such as package name,
share scope, requested version, available versions, selected provider, and a
reason like missing-provider, version-mismatch, or sync-async-boundary.
They do not include shared factories, module values, source, or business data.
Shared observability is intentionally scoped to the Module Federation instance
that resolved the shared dependency. It can answer which MF instance loaded a
shared package, which registered provider/version was selected, and the related
scope/version/eager configuration. It does not guarantee a causal link from that
shared dependency back to a specific remote or expose, because shared resolution
can be triggered later by the bundler runtime while chunks and module factories
execute. When multiple shared dependencies are involved, read all
phase: "shared" events. summary.shared is only a compact last-observed
summary.
