@module-federation/observability-plugin 0.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/README.md ADDED
@@ -0,0 +1,295 @@
1
+ # @module-federation/observability-plugin
2
+
3
+ Runtime observability plugin for Module Federation loading flows.
4
+
5
+ This package is designed for Module Federation `2.5.0` and later. Older
6
+ projects can still use runtime error codes, but the full observability workflow
7
+ requires upgrading the MF runtime and installing this plugin.
8
+
9
+ This package is currently the minimal observability foundation. It records
10
+ structured in-memory loading events when the plugin is installed and not
11
+ disabled. Optional outputs are conservative by default: no browser global, no
12
+ Node file output, and no raw error stack in console output.
13
+
14
+ ```ts
15
+ import { createInstance } from '@module-federation/runtime';
16
+ import { ObservabilityPlugin } from '@module-federation/observability-plugin';
17
+
18
+ const observability = ObservabilityPlugin({
19
+ level: 'verbose',
20
+ browser: {
21
+ enabled: true,
22
+ scope: 'host',
23
+ },
24
+ });
25
+
26
+ createInstance({
27
+ name: 'host',
28
+ plugins: [observability.plugin],
29
+ remotes: [],
30
+ });
31
+
32
+ const report = observability.getLatestReport();
33
+ ```
34
+
35
+ The plugin does not upload data or expose a browser global by default. Reports
36
+ are kept in memory. Runtime request URLs, error messages, and stored error
37
+ stacks keep their original query/hash and error details because those values are
38
+ often needed for debugging. Large deployment locator fields such as
39
+ `publicPath` and `remoteEntry` are only length-limited.
40
+
41
+ ## Safe Observability
42
+
43
+ Enable only the output channels that match the environment:
44
+
45
+ - Browser dev: use `browser.enabled: true` with a scoped reader such as
46
+ `window.__FEDERATION__.__OBSERVABILITY__.host`.
47
+ - Browser prod: set `browser.mode: "production"` so console output stays limited
48
+ to `traceId` and known `errorCode`; export full reports only through explicit
49
+ app-owned flows such as `exportReport()` or `onReport`.
50
+ - Node / SSR: use the Node entry
51
+ `@module-federation/observability-plugin/node` and set `fileOutput: true` when
52
+ local files are needed.
53
+ - Build: use `ObservabilityBuildPlugin` when build-side files are needed for
54
+ later comparison.
55
+
56
+ Reports include the loading timeline, selected host/remote/shared facts,
57
+ original runtime URLs, original error message/stack, clipped deployment
58
+ `moduleInfo` when it is relevant, and user-provided observability
59
+ metadata with count and length limits. Reports do not collect request headers,
60
+ cookies, authorization values, remote response bodies, remote source, module
61
+ source, React props, full `moduleInfo.modules`, full `moduleInfo.shared`, or
62
+ asset lists from deployment `moduleInfo`.
63
+ Fields whose value is `undefined` are omitted from returned reports and events,
64
+ so missing fields should be read as "not observed or not relevant" instead of a
65
+ literal value.
66
+
67
+ Failure hints are printed with `console.error` so browser DevTools, CDP-based
68
+ agents, Node logs, and log collection systems can detect that a Module
69
+ Federation load failed and then use the printed `traceId` to fetch the full
70
+ report. Successful or recovered reports are still available through the reader
71
+ APIs and callbacks, but they are not promoted to console errors.
72
+
73
+ The runtime only exposes the loading lifecycle hooks needed to know whether the
74
+ main flow started, succeeded, or failed. This plugin listens to those hooks,
75
+ derives detailed reasons like shared version mismatch or eager boundary issues,
76
+ and exposes the final loading state through a small `summary` object:
77
+
78
+ - `runtime-loaded`: Module Federation finished loading the remote module.
79
+ - `component-loaded`: business code called `markComponentLoaded`.
80
+ - `failed`: the load failed and `failedPhase` points to the first specific
81
+ failing phase.
82
+ - `recovered`: loading hit an error but a fallback/recovery path returned a
83
+ result.
84
+
85
+ For remote loading, the plugin listens to runtime lifecycle hooks such as
86
+ `beforeRequest`, `afterMatchRemote`, `onLoad`, `afterLoadRemote`,
87
+ `errorLoadRemote`, `loadEntry`, `afterLoadEntry`, `beforeInitRemote`,
88
+ `afterInitRemote`, `beforeGetExpose`, `afterGetExpose`,
89
+ `beforeExecuteFactory`, `afterExecuteFactory`, and snapshot resolve hooks. It
90
+ does not return a value from observer hooks, so it does not change fallback or
91
+ retry results from plugins such as `@module-federation/retry-plugin`.
92
+
93
+ Successful verbose reports include the key runtime stages: remote match,
94
+ manifest, remoteEntry load, remoteEntry init, expose resolution, module factory
95
+ execution, and final load completion. When a stage has matching start and end
96
+ events, the end event includes a bounded `duration` value.
97
+
98
+ The report also keeps compact loading state under `summary`. It contains the
99
+ final outcome, per-phase status and duration under `summary.phases`, safe
100
+ cache/recovery markers under `summary.flags`, and the last resolved shared
101
+ provider/version under `summary.shared` when a shared dependency was observed.
102
+ `level: "summary"` omits start events from the stored timeline but still keeps
103
+ the derived durations on the matching success/error events. `level: "verbose"`
104
+ keeps the full timeline.
105
+
106
+ Each report also includes a deterministic `diagnosis` object. It is generated
107
+ by engineering rules, not by an AI model. It keeps the final outcome, likely
108
+ owner, completed and pending phases, observability facts, documentation link when
109
+ a known runtime error code is present, and a short list of next checks. This is
110
+ the field a person or AI coding agent should read first before falling back to
111
+ the raw `events` timeline.
112
+
113
+ For a stable report-reading and fixing workflow, see
114
+ [`AI_TROUBLESHOOTING.md`](./AI_TROUBLESHOOTING.md). Codex-style agents can also
115
+ use the repository's single `mf` skill entry with the `observability`
116
+ sub-command.
117
+
118
+ `errorLoadShare` is used only for observation. Shared dependency miss, version
119
+ mismatch, and eager boundary errors are not retried by the retry plugin by
120
+ default because they are usually configuration or availability problems instead
121
+ of transient network failures.
122
+
123
+ Business code can mark its own success condition with a fixed event:
124
+
125
+ ```ts
126
+ observability.markComponentLoaded({
127
+ requestId: 'remote/Button',
128
+ componentName: 'Button',
129
+ metadata: {
130
+ route: '/settings',
131
+ },
132
+ });
133
+ ```
134
+
135
+ This records `component:business-loaded` on the same trace when possible.
136
+ Business metadata is optional. String values keep their original details and are
137
+ only length-limited, so user-provided metadata remains trustworthy.
138
+
139
+ Browser output is available only when the plugin option explicitly enables it.
140
+ When browser output is enabled, the report can be read from:
141
+
142
+ ```ts
143
+ window.__FEDERATION__.__OBSERVABILITY__.host.getLatestReport();
144
+ window.__FEDERATION__.__OBSERVABILITY__.host.getReport('mf-trace-id');
145
+ window.__FEDERATION__.__OBSERVABILITY__.host.getReports({ limit: 5 });
146
+ window.__FEDERATION__.__OBSERVABILITY__.host.findReports({ remote: 'remote1' });
147
+ window.__FEDERATION__.__OBSERVABILITY__.host.exportReport('mf-trace-id');
148
+ ```
149
+
150
+ The same methods are available on the observability controller returned by
151
+ `ObservabilityPlugin()`. `getReports({ limit })` returns recent reports newest
152
+ first. `findReports()` can filter by `traceId`, `remote`, `expose`, `shared`,
153
+ `status`, or `outcome`. `exportReport()` returns a copied report object, using
154
+ the latest report when no `traceId` is provided.
155
+
156
+ For browser production use, set `browser.mode: "production"` when the runtime
157
+ console must stay minimal:
158
+
159
+ ```ts
160
+ ObservabilityPlugin({
161
+ browser: {
162
+ enabled: true,
163
+ scope: 'host',
164
+ mode: 'production',
165
+ },
166
+ });
167
+ ```
168
+
169
+ In production browser mode, the `console.error` hint only includes the `traceId`
170
+ and known `errorCode`. It does not print the report body, raw stack, request
171
+ URL, or `read:` command. Full reports are still available only through explicit
172
+ user choices such as the observability controller, `exportReport()`, or an
173
+ application-owned `onReport` upload. Production applications that want richer
174
+ observability should prefer `onReport` / `onEvent` to forward reports to their
175
+ own telemetry system instead of exposing a public browser global.
176
+
177
+ Node file output is provided by the Node-specific entry:
178
+
179
+ ```ts
180
+ import { createInstance } from '@module-federation/runtime';
181
+ import { ObservabilityPlugin } from '@module-federation/observability-plugin/node';
182
+
183
+ const observability = ObservabilityPlugin({
184
+ level: 'verbose',
185
+ fileOutput: true,
186
+ directory: '.mf/observability',
187
+ });
188
+
189
+ createInstance({
190
+ name: 'host',
191
+ plugins: [observability.plugin],
192
+ remotes: [],
193
+ });
194
+ ```
195
+
196
+ When Node file output is explicitly enabled, the Node entry writes:
197
+
198
+ - `.mf/observability/latest.json`: a formatted copy of the latest complete
199
+ report, including `traceId`, top-level status/error fields, `diagnosis`,
200
+ `summary`, clipped `moduleInfo` when relevant, and the report's own `events`.
201
+ - `.mf/observability/events.jsonl`: append-only event stream. Each line is one
202
+ JSON object for one runtime event and includes fields such as `traceId`,
203
+ `timestamp`, `phase`, `status`, remote/shared/expose context, and error
204
+ fields when present.
205
+
206
+ Read `latest.json` first. Use `events.jsonl` only when multiple traces must be
207
+ compared or when the full event ordering for a `traceId` is needed.
208
+
209
+ On errors, the plugin prints a small console hint with the `traceId` and the
210
+ available read path. The console hint is intentionally small and does not carry
211
+ the full report or the raw stack by default. If a user explicitly needs the
212
+ full stack, they can opt in with `printRawStack: true` or capture the original
213
+ error through `onRawError`. The default browser/runtime entry does not include
214
+ Node file output code.
215
+
216
+ Build-time observability is provided by the build-specific entry:
217
+
218
+ ```js
219
+ const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
220
+ const { ObservabilityBuildPlugin } = require('@module-federation/observability-plugin/build');
221
+
222
+ const moduleFederationOptions = {
223
+ name: 'host',
224
+ remotes: {
225
+ remote1: 'remote1@http://localhost:3001/mf-manifest.json',
226
+ },
227
+ exposes: {
228
+ './Button': './src/Button',
229
+ },
230
+ shared: {
231
+ react: { singleton: true, requiredVersion: '^18.0.0' },
232
+ },
233
+ };
234
+
235
+ module.exports = {
236
+ plugins: [
237
+ new ModuleFederationPlugin(moduleFederationOptions),
238
+ new ObservabilityBuildPlugin({
239
+ moduleFederation: moduleFederationOptions,
240
+ }),
241
+ ],
242
+ };
243
+ ```
244
+
245
+ When this optional build plugin is installed, it writes
246
+ `.mf/observability/build-info.json`. The file is a summary of the Module
247
+ Federation build configuration and generated manifest/stats facts:
248
+ bundler name/version, Module Federation plugin version when available, build
249
+ version when available, remoteEntry file/type/publicPath mode, remotes,
250
+ exposes, and shared dependencies. It intentionally omits local expose source
251
+ paths, asset lists, source code, and environment variables. Remote URLs and the
252
+ `remoteEntry.publicPath` deployment locator keep query/hash data. If build
253
+ observability output fails, the build continues and a bundler warning is emitted.
254
+
255
+ When the build has compilation errors, or when the observability plugin cannot
256
+ write its own build output, the build plugin writes
257
+ `.mf/observability/build-report.json`. The report has the same high-level shape
258
+ as runtime reports: a `traceId`, `status`, `failedPhase`, `events`,
259
+ `summary.error`, `diagnosis`, and the top-level `build` object. Clean builds
260
+ remove the stale report file so readers do not mistake an old build error for
261
+ the current state.
262
+
263
+ Runtime reports do not include build facts. `summary` is only the loading-state
264
+ view. If debugging needs build-side evidence, read
265
+ `.mf/observability/build-info.json` or `.mf/observability/build-report.json`
266
+ separately and compare it with the runtime report.
267
+
268
+ For snapshot-dependent failures, such as `RUNTIME-007`, reports can also include
269
+ top-level `moduleInfo`. This field is collected only for failures that depend on
270
+ `__FEDERATION__.moduleInfo`. It is clipped by default: the plugin keeps matching
271
+ entries with only `name`, `publicPath`, `getPublicPath`, `remoteEntry`, and
272
+ `globalName`, keeps the deployment locator fields only length-limited, and
273
+ removes large fields such as `modules` and `shared`.
274
+
275
+ Runtime errors are normalized into stable fields on both events and reports:
276
+
277
+ - `errorCode`: for example `RUNTIME-003` or `RUNTIME-008` when the original
278
+ error includes one.
279
+ - `failedPhase` and `lifecycle`: where the failure happened.
280
+ - `ownerHint`: a deterministic hint such as `host`, `remote`, `shared`, or
281
+ `network`.
282
+ - `retryable`: whether the observed failure looks transient.
283
+ - `errorContext`: a context object with values such as manifest URL,
284
+ remote name, `entryGlobalName`, request id, expose, or shared package.
285
+
286
+ The first batch includes specific diagnosis facts and actions for
287
+ `RUNTIME-001`, `RUNTIME-003`, `RUNTIME-004`, `RUNTIME-005`, `RUNTIME-006`, and
288
+ `RUNTIME-008`. `RUNTIME-008` is further classified as `network`, `timeout`,
289
+ `script-execution`, or `unknown` in `errorContext.resourceErrorType` and
290
+ `diagnosis.facts.resourceErrorType`.
291
+
292
+ Shared dependency reports include only evidence fields such as package name,
293
+ share scope, requested version, available versions, selected provider, and a
294
+ reason like `missing-provider`, `version-mismatch`, or `sync-async-boundary`.
295
+ They do not include shared factories, module values, source, or business data.
@@ -0,0 +1,194 @@
1
+ //#region src/build.d.ts
2
+ type ObservabilityBuildInfoSource = 'config' | 'stats' | 'manifest';
3
+ type ObservabilityBuildPublicPathMode = 'auto' | 'static' | 'runtime-getter' | 'unknown';
4
+ type ObservabilityBuildReportPhase = 'compilation' | 'observability-output';
5
+ type ObservabilityBuildOwnerHint = 'host' | 'remote' | 'shared' | 'build' | 'unknown';
6
+ type ObservabilityBuildMetadata = Record<string, string | number | boolean>;
7
+ interface ObservabilityBuildRemote {
8
+ name?: string;
9
+ alias?: string;
10
+ entry?: string;
11
+ type?: string;
12
+ shareScope?: string | string[];
13
+ }
14
+ interface ObservabilityBuildExpose {
15
+ name: string;
16
+ }
17
+ interface ObservabilityBuildShared {
18
+ name: string;
19
+ shareScope?: string | string[];
20
+ version?: string;
21
+ requiredVersion?: string | false;
22
+ singleton?: boolean;
23
+ strictVersion?: boolean;
24
+ eager?: boolean;
25
+ }
26
+ interface ObservabilityBuildInfo {
27
+ schemaVersion: 1;
28
+ generatedAt: string;
29
+ source: ObservabilityBuildInfoSource;
30
+ bundler: {
31
+ name: string;
32
+ version?: string;
33
+ mode?: string;
34
+ target?: string[];
35
+ };
36
+ moduleFederation: {
37
+ name?: string;
38
+ pluginVersion?: string;
39
+ buildVersion?: string;
40
+ buildName?: string;
41
+ remoteEntry?: {
42
+ name?: string;
43
+ type?: string;
44
+ globalName?: string;
45
+ publicPath?: string;
46
+ publicPathMode: ObservabilityBuildPublicPathMode;
47
+ };
48
+ options: {
49
+ shareStrategy?: string;
50
+ shareScope?: string | string[];
51
+ asyncStartup?: boolean;
52
+ manifest?: boolean | string;
53
+ dts?: boolean;
54
+ };
55
+ remotes: ObservabilityBuildRemote[];
56
+ exposes: ObservabilityBuildExpose[];
57
+ shared: ObservabilityBuildShared[];
58
+ };
59
+ summary: {
60
+ remoteCount: number;
61
+ exposeCount: number;
62
+ sharedCount: number;
63
+ };
64
+ }
65
+ interface ObservabilityBuildErrorSummary {
66
+ errorCode?: string;
67
+ errorName?: string;
68
+ errorMessage?: string;
69
+ failedPhase: ObservabilityBuildReportPhase;
70
+ ownerHint: ObservabilityBuildOwnerHint;
71
+ retryable: false;
72
+ context?: Record<string, string | number | boolean>;
73
+ }
74
+ interface ObservabilityBuildAction {
75
+ id: string;
76
+ ownerHint?: ObservabilityBuildOwnerHint;
77
+ title: string;
78
+ detail?: string;
79
+ }
80
+ interface ObservabilityBuildFactReport {
81
+ title: string;
82
+ outcome: 'failed';
83
+ status: 'error';
84
+ ownerHint: ObservabilityBuildOwnerHint;
85
+ failedPhase: ObservabilityBuildReportPhase;
86
+ errorCode?: string;
87
+ errorName?: string;
88
+ errorMessage?: string;
89
+ facts: ObservabilityBuildMetadata;
90
+ completedPhases: string[];
91
+ pendingPhases: string[];
92
+ actions: ObservabilityBuildAction[];
93
+ }
94
+ interface ObservabilityBuildEvent extends ObservabilityBuildErrorSummary {
95
+ traceId: string;
96
+ timestamp: number;
97
+ phase: 'build';
98
+ status: 'error';
99
+ lifecycle: ObservabilityBuildReportPhase;
100
+ errorStack?: string;
101
+ }
102
+ interface ObservabilityBuildReport {
103
+ schemaVersion: 1;
104
+ traceId: string;
105
+ source: 'build';
106
+ status: 'error';
107
+ startedAt: number;
108
+ updatedAt: number;
109
+ duration: number;
110
+ failedPhase: ObservabilityBuildReportPhase;
111
+ build: ObservabilityBuildInfo;
112
+ events: ObservabilityBuildEvent[];
113
+ summary: {
114
+ eventCount: number;
115
+ outcome: 'failed';
116
+ error?: ObservabilityBuildErrorSummary;
117
+ errors: ObservabilityBuildErrorSummary[];
118
+ };
119
+ diagnosis: ObservabilityBuildFactReport;
120
+ }
121
+ interface CreateObservabilityBuildInfoOptions {
122
+ moduleFederation?: unknown;
123
+ manifest?: unknown;
124
+ stats?: unknown;
125
+ compilerOptions?: Record<string, unknown>;
126
+ bundler?: string;
127
+ bundlerVersion?: string;
128
+ pluginVersion?: string;
129
+ generatedAt?: string;
130
+ }
131
+ interface ObservabilityBuildPluginOptions {
132
+ enabled?: boolean;
133
+ outputFile?: string;
134
+ errorReport?: false | {
135
+ outputFile?: string;
136
+ };
137
+ cwd?: string;
138
+ bundler?: string;
139
+ bundlerVersion?: string;
140
+ pluginVersion?: string;
141
+ moduleFederation?: unknown;
142
+ }
143
+ interface CompilerLike {
144
+ context?: string;
145
+ options?: Record<string, unknown>;
146
+ webpack?: {
147
+ version?: string;
148
+ rspackVersion?: string;
149
+ sources?: {
150
+ RawSource?: new (value: string) => unknown;
151
+ };
152
+ Compilation?: {
153
+ PROCESS_ASSETS_STAGE_REPORT?: number;
154
+ PROCESS_ASSETS_STAGE_SUMMARIZE?: number;
155
+ };
156
+ };
157
+ hooks?: {
158
+ thisCompilation?: {
159
+ tap(name: string, callback: (compilation: CompilationLike) => void): void;
160
+ };
161
+ };
162
+ getInfrastructureLogger?: (name: string) => {
163
+ warn?: (message: string) => void;
164
+ };
165
+ }
166
+ interface CompilationLike {
167
+ constructor?: {
168
+ PROCESS_ASSETS_STAGE_REPORT?: number;
169
+ PROCESS_ASSETS_STAGE_SUMMARIZE?: number;
170
+ };
171
+ hooks?: {
172
+ processAssets?: {
173
+ tapPromise(options: {
174
+ name: string;
175
+ stage?: number;
176
+ }, callback: () => Promise<void>): void;
177
+ };
178
+ };
179
+ getAsset?: (name: string) => {
180
+ source?: unknown;
181
+ } | undefined;
182
+ emitAsset?: (name: string, source: unknown) => void;
183
+ assets?: Record<string, unknown>;
184
+ errors?: unknown[];
185
+ }
186
+ declare function createObservabilityBuildInfo(input: CreateObservabilityBuildInfoOptions): ObservabilityBuildInfo;
187
+ declare class ObservabilityBuildPlugin {
188
+ readonly name = "ObservabilityBuildPlugin";
189
+ private readonly options;
190
+ constructor(options?: ObservabilityBuildPluginOptions);
191
+ apply(compiler: CompilerLike): void;
192
+ }
193
+ //#endregion
194
+ export { CreateObservabilityBuildInfoOptions, ObservabilityBuildAction, ObservabilityBuildErrorSummary, ObservabilityBuildEvent, ObservabilityBuildExpose, ObservabilityBuildFactReport, ObservabilityBuildInfo, ObservabilityBuildInfoSource, ObservabilityBuildMetadata, ObservabilityBuildOwnerHint, ObservabilityBuildPlugin, ObservabilityBuildPluginOptions, ObservabilityBuildPublicPathMode, ObservabilityBuildRemote, ObservabilityBuildReport, ObservabilityBuildReportPhase, ObservabilityBuildShared, createObservabilityBuildInfo };