# @lensmcp/react-instrumentation

Zero-config React instrumentation for LensMCP — a Babel transform plus a render/flow runtime that lets the LensMCP lens *see* a running React app: which components rendered, which hooks re-ran, and how a click in the browser stitches together with the server request and DB query it triggered.

This package is the React-side half of LensMCP's observability lens. A small **Babel plugin** rewrites your JSX at build time (stamping component identity, wrapping `useEffect`/`useMemo`/`useCallback`, wrapping interaction handlers, and wrapping the root render). A matching **runtime** — traced hooks, a `<LensmcpRoot>` wrapper, and a `fetch` wrapper (*flow-fetch*) — emits structured events and propagates a `flowId` across the network boundary so a single user action can be read end to end (click → fetch → server span → DB query).

You normally don't import any of this by hand. [`@lensmcp/vite-plugin`](https://github.com/kiwiapps-ltd/lensmcp) wires the Babel plugin into your React build, injects the publish channel into the served HTML, and bridges the emitted events to the lens. The exports below are available for projects that can't run the global transform (e.g. opt-in tracing) or want to start a flow manually.

## Install

```sh
yarn add @lensmcp/react-instrumentation
```

In practice you don't add it directly — it's pulled in and configured by **`@lensmcp/vite-plugin`**, which applies the Babel plugin and sets up the transport. Install it explicitly only for the opt-in, hand-wired path described under [Usage](#usage).

## Usage

**The realistic path (automatic).** Add `@lensmcp/vite-plugin` to your Vite config. It loads this package's Babel plugin, so every component, hook, conditional, and interaction handler in your source is instrumented with no app-code changes — and it installs the publish channel that the runtime emits into.

If you can't run the global transform, the runtime is usable directly:

```tsx
import { createRoot } from 'react-dom/client';
import { LensmcpRoot, installFlowFetch } from '@lensmcp/react-instrumentation';

// Wrap browser fetches so they carry the active flow's headers.
installFlowFetch();

createRoot(document.getElementById('root')!).render(
  <LensmcpRoot page="home" route="/">
    <App />
  </LensmcpRoot>,
);
```

(This is exactly what the Babel plugin generates automatically when `autoWrapRoot` is on.) From there you can opt individual pieces in:

```tsx
import { withTraceComponent, withFlow, tracedUseEffect } from '@lensmcp/react-instrumentation';

// Trace a single component's renders without the global transform.
const TracedPanel = withTraceComponent(Panel, { source: 'src/Panel.tsx' });

// Start a correlated flow from a handler the transform didn't reach.
<button onClick={withFlow(save, { originType: 'user-click', originNodeId: 'ui:save' })}>
  Save
</button>;

// Drop-in useEffect that emits effect-run / effect-cleanup events.
tracedUseEffect('Panel:42:0', () => subscribe(), [topic]);
```

If you build your own bundler integration, point the runtime at your transport with `setPublisher(fn)`; until a publisher is set, every event is buffered (capped) so the library is safe to import in tests and SSR.

## What it captures

- **Component renders** — the Babel transform stamps each JSX element with `data-agent-component` (the component name) and `data-agent-source` (`file:line`), and wraps the root in `<LensmcpRoot>`, whose React `<Profiler>` emits a `render` event (mount/update, durations, commit times) on every commit. `withTraceComponent` does the same for a single opt-in component.
- **Hooks** — `useEffect`/`useMemo`/`useCallback` are rewritten to traced variants that emit `effect-run` (plus `effect-cleanup`) and `hook-run` events, including a cheap `depsHash` so the lens can tell whether a hook re-ran because its dependencies changed.
- **Conditional slots** — `<TraceSlot>` emits `slot-content-changed` when the type of the rendered child changes (e.g. a branch swap in a JSX conditional).
- **Browser→server flow correlation (flow-fetch)** — interaction handlers are wrapped with `withFlow`, which opens a flow with a generated `flowId`. The patched `fetch` injects `x-lensmcp-flow-id` + `x-lensmcp-origin-node-id` headers so the server-side `@lensmcp/nest-instrumentation` stamps its spans and DB queries with the *same* flowId, and it publishes `fetch-start` / `fetch-done` / `fetch-error` legs. A short causality window (and an auto-started `page-load` flow) attributes async post-response re-renders to the action that caused them.
- **Identity** — stable `logicalId`s for components, hooks, and slots plus per-mount `instanceId`s, so the lens can attribute every event to a specific component instance across re-renders.

## API

Exports from the package entry (`@lensmcp/react-instrumentation`):

| Export | Description |
| --- | --- |
| `LensmcpRoot` / `LensmcpRootProps` | Root wrapper: a `<Profiler>` + context provider that emits `render` events and tags hooks with renderer/page/route. |
| `LensmcpContext` / `useLensmcpContext` / `LensmcpIdentityContext` | React context (and hook) carrying the active renderer/page/route and component instance identity. |
| `tracedUseEffect` / `tracedUseMemo` / `tracedUseCallback` | Drop-in hook replacements that emit `effect-run`/`hook-run` events; the transform rewrites bare hooks to these. |
| `TraceSlot` / `TraceSlotProps` | Wrapper that emits `slot-content-changed` when its primary child's type changes. |
| `withTraceComponent` / `WithTraceOptions` | HOC for opt-in render tracing when the global transform isn't available. |
| `installFlowFetch` / `FlowFetchOptions` | Monkeypatches `globalThis.fetch` to inject flow headers and publish network legs; returns an uninstall function. |
| `withFlow` | Wraps an event handler so each call runs inside a fresh correlated flow (idempotent; passes non-functions through). |
| `runInFlow` / `beginBackgroundFlow` / `currentFlow` / `activeFlow` / `extendFlowWindow` | Flow-stack and causality-window primitives for starting/inspecting/extending flows manually. |
| `publish` / `setPublisher` / `drainQueue` | The publish channel: emit an event, install the transport, or drain the buffered queue (tests). |
| `PublishEnvelope` / `PublishCategory` / `FlowSpec` | Types for the publish envelope, event category, and a flow descriptor. |
| `componentLogicalId` / `hookLogicalId` / `slotLogicalId` / `makeInstanceId` / `depsHash` / `resetIdentityState` | Identity helpers — build stable logical ids, per-mount instance ids, and dependency hashes. |
| `RenderRecord` / `EffectRecord` / `HookRecord` / `SlotChangeRecord` / `ComponentMountRecord` / `ReactPublishPayload` | Wire-shape types for the events the library publishes. |

Subpath export `@lensmcp/react-instrumentation/babel-plugin` (also re-exported from the entry as `lensmcpBabelPlugin`, with `LensmcpBabelOptions`): the JSX identity transform. Options: `production`, `includeSource`, `rootDir`, `autoWrapHooks`, `autoWrapRoot`, `autoFlowHandlers`, `flowEvents`. It's dev-only — a no-op when `production` is true or `NODE_ENV === 'production'` — and honors `// @lensmcp-ignore` (file) and `data-lensmcp-ignore` (element) opt-outs.

```ts
// vite.config.ts — applied for you by @lensmcp/vite-plugin
import lensmcpBabel from '@lensmcp/react-instrumentation/babel-plugin';
react({ babel: { plugins: [lensmcpBabel] } });
```

## How it fits

- **Configured by [`@lensmcp/vite-plugin`](https://github.com/kiwiapps-ltd/lensmcp).** The Vite plugin runs `lensmcpBabelPlugin` over your React source and injects a client runtime that sets `window.__LENSMCP_PUBLISH__` — the transport this package's `publish()` looks for. In dev, events flow over a WebSocket to the lens; in production, over the FrontMCP server transport.
- **Pairs with [`@lensmcp/valtio-instrumentation`](https://github.com/kiwiapps-ltd/lensmcp).** The Vite plugin aliases `valtio` → `@lensmcp/valtio-instrumentation` so state mutations publish into the same channel, and `flowId` correlation lets a render here be tied to the state change (and the fetch) that caused it.
- **Server side: [`@lensmcp/nest-instrumentation`](https://github.com/kiwiapps-ltd/lensmcp).** Its `TraceInterceptor` reads the `x-lensmcp-flow-id` header set by flow-fetch and stamps server-request, span, and DB-query events with the matching flowId — completing the browser→server story.
- **Events use [`@lensmcp/protocol-types`](https://github.com/kiwiapps-ltd/lensmcp).** The published record shapes (`RenderPhase`, `RenderWhy`, etc.) mirror the shared protocol so the lens and reducers can consume them.

---

Part of [LensMCP](https://github.com/kiwiapps-ltd/lensmcp). Apache-2.0.
