Five instrumentation steps can turn a multi-root state race into a traceable timeline across micro-frontends. Debug it cleanly. Full breakdown.
What a Hydra Bug Looks Like
A micro-frontend hydra bug is a state race that spans more than one root. Each shell or remote owns its own store, cache, or event bus. From any single root the behavior looks fine: a flag flips, a selector returns the expected value, a subscriber runs. Across roots, the same user action yields different outcomes depending on which mount finished last, which bus message arrived first, or which shared cookie or storage key was rewritten by a neighbor. The bug multiplies heads because each root only sees its own slice of truth.
You cannot fix what you cannot order. Debugging distributed UI state starts by forcing a shared timeline, not by adding more console logs inside one package.
Five Instrumentation Steps for a Shared Timeline
Treat every root as a peer that must emit the same class of events into one ordered stream. Keep the payload small and consistent so you can merge logs without guessing which field means what.
- Identity every root at boot. Assign a stable root id, version or build id, and mount path. Emit a single “root_ready” event when the store and bus are live. Without this, late mounts look like random flakiness.
- Stamp a shared clock. Prefer a monotonic sequence from the host (or a host-injected counter) over wall-clock times alone. Wall clocks drift; a host sequence makes interleaving readable.
- Trace user intent once. Generate a correlation id at the shell for navigation, form submit, or feature toggle. Pass it into remotes via props, URL, or bus metadata so every subsequent state change can be grouped.
- Log state transitions, not only side effects. For each store, emit before/after for the fields that drive UI (auth, cart, feature flags, selected entity). Include the reducer or action name when you have one. Skip noisy intermediate renders.
- Mirror bus and storage writes. Cross-root races often hide in custom events, broadcast channels, localStorage, or sessionStorage. Log writer root, key, previous value hash, and new value hash on the same stream as store updates.
Reading the Timeline Instead of Guessing
Once events share root id, sequence, and correlation id, sort the merged log by sequence (then by root id for ties). Walk a single user intent from first click to final paint. Look for double writes to the same key, a remote applying an event after another root already invalidated it, or a selector reading stale props because the parent re-rendered on a different bus message. The “hydra” usually collapses into a short pattern: two writers, one reader, no ownership rule.
Capture a failing path once with instrumentation on, then turn high-volume logs down. Permanent full-state dumps will drown the signal and can leak sensitive fields; prefer hashed values and field allowlists in anything that leaves the developer machine.
Closing the Race After You Can See It
Instrumentation does not replace design. After the timeline shows the conflict, pick one owner for each shared concern—host-owned auth, a single cart remote, or an explicit “last write wins only for keys X.” Prefer one-way data flow from host into remotes for global flags, and keep remotes authoritative only for local UI. If two roots must write the same resource, serialize through a host mediator or a single bus handler with a clear precedence rule.
Ship the five hooks as a thin debug layer you can enable per environment. When a multi-root race returns, you re-enable the stream, reproduce once, and read an ordered story instead of arguing about which micro-frontend “should have” been right.