telemetry-otel
This feature shows you what your micro-frontends are doing in real browsers. Spans, fetch requests, Core Web Vitals and remote load failures go to an OTLP endpoint, and each one says which remote it came from. The OpenTelemetry SDK loads after the page has painted, so it stays off the path to first render.
What you get
A shell that starts telemetry from its runtime config, and a tracer per remote that tags every span with mfe.name. Failed remote loads and render errors are reported without any wiring in your remotes. It is off by default, and with no endpoint set the SDK is never downloaded.
Add it
What changed:
The @mfeflow/telemetry package is added to the shell and every remote. Three keys are added to the runtime config, described under "Configure it" below. If you have the testing feature, each file gets a unit test next to it.
Use it
Turn it on
Point the shell at an OTLP/HTTP traces URL in the environment that should send data:
Start a trace from a click
The generated file in each remote exports a tracer, named after the remote:
Use it where something worth timing happens:
Start the fetch inside the callback, before any await. The reason is in "How it works" below.
Know when a remote fails
You do not write anything for this. When a remote fails to load or throws while rendering, @mfeflow/runtime reports it on window. Telemetry turns it into an mfe.error span tagged with the remote's name.
The span also says where it failed:
Failures that happened before telemetry started are replayed.
Configure it
These live in the runtime config, and pnpm check:runtime-config checks the URL, the ratio and the list. The feature has no install prompts.
More options are on createOtelProvider in apps/shell/src/telemetry/init.ts:
How it works
initTelemetry takes a function that returns the provider. The SDK chunk is requested after the page has loaded, the next frame has painted and the browser is idle, or after 3 seconds if the tab is hidden and never paints. Until then, error reports and web vitals are queued (the last 50) and replayed. Spans started before the provider is ready are not recorded.
Once running, getTracer(name) stamps each span with mfe.name, and with mfe.version if you pass one. Spans created without a name, such as the fetch span from the instrumentation, inherit their parent's. A trace therefore reads by remote.
Resource attributes on every span:
Core Web Vitals become web_vital.* spans tagged mfe.name = shell:
Same-origin fetch requests get a traceparent header. Other origins get one only if they match a prefix in propagateTraceHeaderCorsUrls, and that server must allow the header in CORS. Requests to the OTLP endpoint itself are never traced.
What leaves the browser
Spans are scrubbed before export, because tokens and email addresses end up in URLs.
Userinfo and the fragment are always removed. Each reported error sends at most 4 KB of message and 4 KB of stack.
Why the context manager is a stack
A fetch can only be a child of the click span if the SDK knows which span is active when it runs. That is the context manager's job.
We measured it on 2026-11-01 in Chrome, with the fetch instrumentation and these versions:
The default is the stack manager. zone.js has to patch promises and timers before application code runs, so it would load on the critical path.
Without downleveling, zone fixes .then() and timers but not native await, which modern code uses. Downleveling to ES2016 is the only setup where zone is right everywhere, and it changes how every app compiles. Starting the fetch inside the callback works with both and costs nothing.
Gotchas
If you need a span to survive an await, capture the context before it and run the call in it with context.with(context.active(), ...) from @opentelemetry/api in the shell. Then hand your remotes a function that does it.
-
Keeping the query string. Turn on
recordQueryonly for a backend you trust with whatever your URLs carry. Userinfo and the fragment are still removed. -
Adding env-t3 later. The keys move into the schema and the environment files. Delete the old shared file, or doctor reports it as missing
environment: -
Versions. The OpenTelemetry JavaScript SDK split into two lines in October 2026. This feature uses the 3.x packages with the API at 1.10. The exporter and the fetch instrumentation (0.300.0) are experimental packages.
Only
@mfeflow/telemetry/otelimports the experimental ones.
Reference
- env-t3, where the three keys are declared and checked
- Runtime config
- Cost model, for what shared code costs a page
@mfeflow/telemetryREADME