@lensmcp/react-instrumentation 1.0.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/LICENSE +202 -0
- package/README.md +100 -0
- package/index.d.ts +11 -0
- package/index.d.ts.map +1 -0
- package/index.js +9 -0
- package/lib/babel-plugin.d.ts +59 -0
- package/lib/babel-plugin.d.ts.map +1 -0
- package/lib/babel-plugin.js +259 -0
- package/lib/context.d.ts +15 -0
- package/lib/context.d.ts.map +1 -0
- package/lib/context.js +6 -0
- package/lib/flow-fetch.d.ts +8 -0
- package/lib/flow-fetch.d.ts.map +1 -0
- package/lib/flow-fetch.js +121 -0
- package/lib/hoc.d.ts +13 -0
- package/lib/hoc.d.ts.map +1 -0
- package/lib/hoc.js +45 -0
- package/lib/identity.d.ts +23 -0
- package/lib/identity.d.ts.map +1 -0
- package/lib/identity.js +74 -0
- package/lib/lensmcp-root.d.ts +22 -0
- package/lib/lensmcp-root.d.ts.map +1 -0
- package/lib/lensmcp-root.js +53 -0
- package/lib/publish.d.ts +89 -0
- package/lib/publish.d.ts.map +1 -0
- package/lib/publish.js +174 -0
- package/lib/trace-slot.d.ts +12 -0
- package/lib/trace-slot.d.ts.map +1 -0
- package/lib/trace-slot.js +67 -0
- package/lib/traced-hooks.d.ts +12 -0
- package/lib/traced-hooks.d.ts.map +1 -0
- package/lib/traced-hooks.js +143 -0
- package/lib/types.d.ts +83 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +1 -0
- package/package.json +64 -0
package/lib/publish.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const QUEUE = [];
|
|
2
|
+
const MAX_QUEUE = 200;
|
|
3
|
+
const FLOW_STACK = [];
|
|
4
|
+
/**
|
|
5
|
+
* Causality window: the synchronous flow stack misses everything Valtio and
|
|
6
|
+
* React deliver asynchronously — subscriber notifications, Profiler commits,
|
|
7
|
+
* and the whole post-response wave (state update + re-render after a fetch).
|
|
8
|
+
* An interaction OPENS a short attribution window; fetch completion EXTENDS
|
|
9
|
+
* it; any publish that has no explicit flow and no sync stack inherits the
|
|
10
|
+
* window's flow. Single-user dev semantics: the most recent interaction owns
|
|
11
|
+
* ambient activity for a few seconds — exactly how a person reads the app.
|
|
12
|
+
*/
|
|
13
|
+
const FLOW_WINDOW_MS = 4000;
|
|
14
|
+
let windowFlow;
|
|
15
|
+
export function extendFlowWindow(spec) {
|
|
16
|
+
const s = spec ?? currentFlow() ?? windowFlow?.spec;
|
|
17
|
+
if (s)
|
|
18
|
+
windowFlow = { spec: s, until: Date.now() + FLOW_WINDOW_MS };
|
|
19
|
+
}
|
|
20
|
+
function windowedFlow() {
|
|
21
|
+
return windowFlow && Date.now() < windowFlow.until ? windowFlow.spec : undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The flow that owns work happening NOW: sync stack first, else the window. */
|
|
24
|
+
export function activeFlow() {
|
|
25
|
+
return currentFlow() ?? windowedFlow();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start a flow with no synchronous handler to wrap — page loads, timers,
|
|
29
|
+
* websocket pushes. Opens the causality window (so the async work that
|
|
30
|
+
* follows inherits the flow) and publishes the origin event that makes the
|
|
31
|
+
* flow exist in the reducer.
|
|
32
|
+
*/
|
|
33
|
+
export function beginBackgroundFlow(origin) {
|
|
34
|
+
const spec = {
|
|
35
|
+
flowId: newFlowId(),
|
|
36
|
+
originType: origin.originType,
|
|
37
|
+
...(origin.originNodeId ? { originNodeId: origin.originNodeId } : {}),
|
|
38
|
+
};
|
|
39
|
+
windowFlow = { spec, until: Date.now() + FLOW_WINDOW_MS };
|
|
40
|
+
publish({
|
|
41
|
+
source: 'react',
|
|
42
|
+
category: 'runtime',
|
|
43
|
+
severity: 'info',
|
|
44
|
+
title: origin.originType,
|
|
45
|
+
fingerprint: `flow-origin:${origin.originNodeId ?? origin.originType}`,
|
|
46
|
+
raw: { kind: 'flow-origin', originNodeId: origin.originNodeId },
|
|
47
|
+
});
|
|
48
|
+
return spec;
|
|
49
|
+
}
|
|
50
|
+
export function setPublisher(fn) {
|
|
51
|
+
globalThis.__LENSMCP_PUBLISH__ = fn;
|
|
52
|
+
if (fn) {
|
|
53
|
+
while (QUEUE.length) {
|
|
54
|
+
const ev = QUEUE.shift();
|
|
55
|
+
if (ev) {
|
|
56
|
+
try {
|
|
57
|
+
fn(ev);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* swallow */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Push a flow onto the module-level stack for the duration of `fn`. Any
|
|
68
|
+
* `publish()` calls inside `fn` (including transitively triggered
|
|
69
|
+
* Valtio subscribers and React Profiler callbacks that fire synchronously)
|
|
70
|
+
* will inherit the flow context.
|
|
71
|
+
*
|
|
72
|
+
* NB: only synchronous propagation. For async chains (`fetch().then`,
|
|
73
|
+
* `setTimeout`, promises) the caller must re-enter the flow with
|
|
74
|
+
* `runInFlow` at the continuation site. The Phase 6.5 carryover adds
|
|
75
|
+
* automatic continuation tracking via an async-context shim.
|
|
76
|
+
*/
|
|
77
|
+
export function runInFlow(spec, fn) {
|
|
78
|
+
FLOW_STACK.push(spec);
|
|
79
|
+
windowFlow = { spec, until: Date.now() + FLOW_WINDOW_MS }; // interaction opens the causality window
|
|
80
|
+
try {
|
|
81
|
+
return fn();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
FLOW_STACK.pop();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function currentFlow() {
|
|
88
|
+
return FLOW_STACK[FLOW_STACK.length - 1];
|
|
89
|
+
}
|
|
90
|
+
/** Marks a handler as already flow-wrapped so layered wrapping is a no-op. */
|
|
91
|
+
const FLOWED = Symbol.for('lensmcp.flowed');
|
|
92
|
+
/**
|
|
93
|
+
* Wrap a React event handler so every call runs inside a fresh flow.
|
|
94
|
+
* The flowId is generated per invocation; `originType` defaults to
|
|
95
|
+
* `'user-click'`. Pass `originNodeId` from the component identity
|
|
96
|
+
* (typically the `data-agent-component` value).
|
|
97
|
+
*
|
|
98
|
+
* The babel transform wraps `on*` JSX values blindly, so this must be
|
|
99
|
+
* total: non-function values (e.g. `onClick={maybeUndefined}`) pass
|
|
100
|
+
* through unchanged, and an already-flowed handler is returned as-is —
|
|
101
|
+
* layered components forwarding the same prop would otherwise nest a
|
|
102
|
+
* flow per layer and crash on the inner non-function (`handler is not
|
|
103
|
+
* a function`).
|
|
104
|
+
*/
|
|
105
|
+
export function withFlow(handler, spec = {}) {
|
|
106
|
+
if (typeof handler !== 'function')
|
|
107
|
+
return handler;
|
|
108
|
+
const existing = handler;
|
|
109
|
+
if (existing[FLOWED])
|
|
110
|
+
return handler;
|
|
111
|
+
const flowed = function flowed(...args) {
|
|
112
|
+
const flowId = newFlowId();
|
|
113
|
+
return runInFlow({
|
|
114
|
+
flowId,
|
|
115
|
+
originType: spec.originType ?? 'user-click',
|
|
116
|
+
originNodeId: spec.originNodeId,
|
|
117
|
+
}, () => {
|
|
118
|
+
// The origin event — published inside the flow so it carries the
|
|
119
|
+
// flowId. It guarantees the flow materialises in the reducer (which
|
|
120
|
+
// keys on the first event with a flowId) even when every downstream
|
|
121
|
+
// effect lands asynchronously, outside the synchronous flow window.
|
|
122
|
+
publish({
|
|
123
|
+
source: 'react',
|
|
124
|
+
category: 'runtime',
|
|
125
|
+
severity: 'info',
|
|
126
|
+
title: spec.originType ?? 'user-click',
|
|
127
|
+
fingerprint: `flow-origin:${spec.originNodeId ?? 'unknown'}`,
|
|
128
|
+
raw: { kind: 'flow-origin', originNodeId: spec.originNodeId },
|
|
129
|
+
});
|
|
130
|
+
return handler(...args);
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
flowed[FLOWED] = true;
|
|
134
|
+
return flowed;
|
|
135
|
+
}
|
|
136
|
+
let flowSeq = 0;
|
|
137
|
+
function newFlowId() {
|
|
138
|
+
flowSeq = (flowSeq + 1) % 0xffff;
|
|
139
|
+
return `flow:${Date.now().toString(36)}:${flowSeq.toString(36)}`;
|
|
140
|
+
}
|
|
141
|
+
export function publish(env) {
|
|
142
|
+
// Sync stack first; otherwise the causality window — unless the caller
|
|
143
|
+
// already carries its own flow (e.g. fetch-done with closure context).
|
|
144
|
+
const flow = currentFlow() ?? (env.context?.flowId ? undefined : windowedFlow());
|
|
145
|
+
const stamped = flow == null
|
|
146
|
+
? env
|
|
147
|
+
: {
|
|
148
|
+
...env,
|
|
149
|
+
context: {
|
|
150
|
+
...(flow.flowId ? { flowId: flow.flowId } : {}),
|
|
151
|
+
...(flow.originType ? { originType: flow.originType } : {}),
|
|
152
|
+
...(flow.originNodeId ? { originNodeId: flow.originNodeId } : {}),
|
|
153
|
+
...(flow.causedByNodeId ? { causedByNodeId: flow.causedByNodeId } : {}),
|
|
154
|
+
...env.context,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const fn = globalThis.__LENSMCP_PUBLISH__;
|
|
158
|
+
if (fn) {
|
|
159
|
+
try {
|
|
160
|
+
fn(stamped);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
/* fall through to queue */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (QUEUE.length >= MAX_QUEUE)
|
|
168
|
+
QUEUE.shift();
|
|
169
|
+
QUEUE.push(stamped);
|
|
170
|
+
}
|
|
171
|
+
/** Test hook — only useful in unit tests / smokes. */
|
|
172
|
+
export function drainQueue() {
|
|
173
|
+
return QUEUE.splice(0, QUEUE.length);
|
|
174
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface TraceSlotProps {
|
|
3
|
+
id: string;
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Slot tracker. Emits `slot-content-changed` when the type of the
|
|
8
|
+
* primary child changes. The Babel transform (T5.1) wraps JSX
|
|
9
|
+
* conditionals with `<TraceSlot id="file:line:conditional">{...}</TraceSlot>`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function TraceSlot(props: TraceSlotProps): ReactNode;
|
|
12
|
+
//# sourceMappingURL=trace-slot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-slot.d.ts","sourceRoot":"","sources":["../../src/lib/trace-slot.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,SAAS,EAAqB,MAAM,OAAO,CAAC;AAK9E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAsC1D"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Children, Fragment, useEffect, useRef } from 'react';
|
|
3
|
+
import { publish } from './publish.js';
|
|
4
|
+
import { makeInstanceId } from './identity.js';
|
|
5
|
+
import { useLensmcpContext } from './context.js';
|
|
6
|
+
/**
|
|
7
|
+
* Slot tracker. Emits `slot-content-changed` when the type of the
|
|
8
|
+
* primary child changes. The Babel transform (T5.1) wraps JSX
|
|
9
|
+
* conditionals with `<TraceSlot id="file:line:conditional">{...}</TraceSlot>`.
|
|
10
|
+
*/
|
|
11
|
+
export function TraceSlot(props) {
|
|
12
|
+
const ctx = useLensmcpContext();
|
|
13
|
+
const lastTypeRef = useRef(undefined);
|
|
14
|
+
const slotInstanceRef = useRef(undefined);
|
|
15
|
+
if (!slotInstanceRef.current) {
|
|
16
|
+
slotInstanceRef.current = makeInstanceId(`react:slot:${props.id}`).instanceId;
|
|
17
|
+
}
|
|
18
|
+
const slotInstanceId = slotInstanceRef.current;
|
|
19
|
+
const currentType = describeChild(props.children);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (lastTypeRef.current !== currentType) {
|
|
22
|
+
const fromComponent = lastTypeRef.current;
|
|
23
|
+
lastTypeRef.current = currentType;
|
|
24
|
+
publish({
|
|
25
|
+
source: 'react',
|
|
26
|
+
category: 'render',
|
|
27
|
+
severity: 'info',
|
|
28
|
+
title: `slot ${props.id}: ${fromComponent ?? '(none)'} → ${currentType}`,
|
|
29
|
+
fingerprint: `slot-change:${props.id}`,
|
|
30
|
+
raw: {
|
|
31
|
+
kind: 'slot-content-changed',
|
|
32
|
+
slot: {
|
|
33
|
+
slotLogicalId: `react:slot:${props.id}`,
|
|
34
|
+
slotInstanceId,
|
|
35
|
+
fromComponent,
|
|
36
|
+
toComponent: currentType,
|
|
37
|
+
componentInstanceId: ctx.componentInstanceId,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}, [currentType, props.id, slotInstanceId, ctx.componentInstanceId]);
|
|
43
|
+
// Stable wrapper so React doesn't think the children's parent changed
|
|
44
|
+
// when the slot remounts — we want layout to feel transparent.
|
|
45
|
+
return _jsx(Fragment, { children: props.children });
|
|
46
|
+
}
|
|
47
|
+
function describeChild(children) {
|
|
48
|
+
const arr = Children.toArray(children);
|
|
49
|
+
const first = arr[0];
|
|
50
|
+
if (!first)
|
|
51
|
+
return '(empty)';
|
|
52
|
+
if (typeof first === 'string' || typeof first === 'number')
|
|
53
|
+
return '(text)';
|
|
54
|
+
if (typeof first === 'boolean')
|
|
55
|
+
return '(boolean)';
|
|
56
|
+
if (Array.isArray(first))
|
|
57
|
+
return '(array)';
|
|
58
|
+
if (typeof first === 'object' && first && 'type' in first) {
|
|
59
|
+
const t = first.type;
|
|
60
|
+
if (typeof t === 'string')
|
|
61
|
+
return t;
|
|
62
|
+
if (typeof t === 'function')
|
|
63
|
+
return t.displayName ?? t.name ?? 'AnonymousComponent';
|
|
64
|
+
return 'unknown';
|
|
65
|
+
}
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop-in replacement for `useEffect` that emits an `effect-run` event
|
|
3
|
+
* on every run + a separate event on cleanup. The transform (T5.1)
|
|
4
|
+
* rewrites bare `useEffect(fn, deps)` calls to
|
|
5
|
+
* `tracedUseEffect("<file>:<line>:<index>", fn, deps)`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function tracedUseEffect(hookLogicalId: string, effect: () => void | (() => void), deps: readonly unknown[] | undefined): void;
|
|
8
|
+
/** Same idea for `useMemo` — emits a `hook-run` event with `recomputed`. */
|
|
9
|
+
export declare function tracedUseMemo<T>(hookLogicalId: string, compute: () => T, deps: readonly unknown[] | undefined): T;
|
|
10
|
+
/** Drop-in `useCallback` with the same accounting as `tracedUseMemo`. */
|
|
11
|
+
export declare function tracedUseCallback<T extends (...args: never[]) => unknown>(hookLogicalId: string, cb: T, deps: readonly unknown[] | undefined): T;
|
|
12
|
+
//# sourceMappingURL=traced-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traced-hooks.d.ts","sourceRoot":"","sources":["../../src/lib/traced-hooks.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,EACjC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,GACnC,IAAI,CA6DN;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,CAAC,EAC7B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,CAAC,EAChB,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,GACnC,CAAC,CA8BH;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,EACvE,aAAa,EAAE,MAAM,EACrB,EAAE,EAAE,CAAC,EACL,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,GACnC,CAAC,CA8BH"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useCallback as reactUseCallback, useEffect as reactUseEffect, useMemo as reactUseMemo, useRef, } from 'react';
|
|
2
|
+
import { useLensmcpContext } from './context.js';
|
|
3
|
+
import { depsHash } from './identity.js';
|
|
4
|
+
import { publish } from './publish.js';
|
|
5
|
+
/**
|
|
6
|
+
* Drop-in replacement for `useEffect` that emits an `effect-run` event
|
|
7
|
+
* on every run + a separate event on cleanup. The transform (T5.1)
|
|
8
|
+
* rewrites bare `useEffect(fn, deps)` calls to
|
|
9
|
+
* `tracedUseEffect("<file>:<line>:<index>", fn, deps)`.
|
|
10
|
+
*/
|
|
11
|
+
export function tracedUseEffect(hookLogicalId, effect, deps) {
|
|
12
|
+
const ctx = useLensmcpContext();
|
|
13
|
+
const hookInstanceId = useStableHookInstance(hookLogicalId);
|
|
14
|
+
const prevHashRef = useRef(undefined);
|
|
15
|
+
reactUseEffect(() => {
|
|
16
|
+
const startedAt = typeof performance !== 'undefined' && performance.now
|
|
17
|
+
? performance.now()
|
|
18
|
+
: Date.now();
|
|
19
|
+
const hash = depsHash(deps);
|
|
20
|
+
const prevHash = prevHashRef.current;
|
|
21
|
+
prevHashRef.current = hash;
|
|
22
|
+
let cleanup;
|
|
23
|
+
try {
|
|
24
|
+
cleanup = effect();
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
const endedAt = typeof performance !== 'undefined' && performance.now
|
|
28
|
+
? performance.now()
|
|
29
|
+
: Date.now();
|
|
30
|
+
publish({
|
|
31
|
+
source: 'react',
|
|
32
|
+
category: 'render',
|
|
33
|
+
severity: 'info',
|
|
34
|
+
title: `effect-run ${hookLogicalId}`,
|
|
35
|
+
fingerprint: `effect-run:${hookLogicalId}`,
|
|
36
|
+
raw: {
|
|
37
|
+
kind: 'effect-run',
|
|
38
|
+
effect: {
|
|
39
|
+
hookInstanceId,
|
|
40
|
+
hookLogicalId,
|
|
41
|
+
componentInstanceId: ctx.componentInstanceId,
|
|
42
|
+
depsHash: hash,
|
|
43
|
+
prevDepsHash: prevHash,
|
|
44
|
+
durationMs: Math.max(0, endedAt - startedAt),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
if (typeof cleanup === 'function')
|
|
51
|
+
cleanup();
|
|
52
|
+
publish({
|
|
53
|
+
source: 'react',
|
|
54
|
+
category: 'render',
|
|
55
|
+
severity: 'info',
|
|
56
|
+
title: `effect-cleanup ${hookLogicalId}`,
|
|
57
|
+
fingerprint: `effect-cleanup:${hookLogicalId}`,
|
|
58
|
+
raw: {
|
|
59
|
+
kind: 'effect-run',
|
|
60
|
+
effect: {
|
|
61
|
+
hookInstanceId,
|
|
62
|
+
hookLogicalId,
|
|
63
|
+
componentInstanceId: ctx.componentInstanceId,
|
|
64
|
+
depsHash: prevHashRef.current ?? hash,
|
|
65
|
+
durationMs: 0,
|
|
66
|
+
cleanupRan: true,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
}, deps);
|
|
72
|
+
}
|
|
73
|
+
/** Same idea for `useMemo` — emits a `hook-run` event with `recomputed`. */
|
|
74
|
+
export function tracedUseMemo(hookLogicalId, compute, deps) {
|
|
75
|
+
const ctx = useLensmcpContext();
|
|
76
|
+
const hookInstanceId = useStableHookInstance(hookLogicalId);
|
|
77
|
+
const prevHashRef = useRef(undefined);
|
|
78
|
+
const hash = depsHash(deps);
|
|
79
|
+
const recomputed = hash !== prevHashRef.current;
|
|
80
|
+
const value = reactUseMemo(compute, deps ?? []);
|
|
81
|
+
if (recomputed) {
|
|
82
|
+
publish({
|
|
83
|
+
source: 'react',
|
|
84
|
+
category: 'render',
|
|
85
|
+
severity: 'debug',
|
|
86
|
+
title: `useMemo recomputed ${hookLogicalId}`,
|
|
87
|
+
fingerprint: `hook-run:${hookLogicalId}`,
|
|
88
|
+
raw: {
|
|
89
|
+
kind: 'hook-run',
|
|
90
|
+
hook: {
|
|
91
|
+
hookInstanceId,
|
|
92
|
+
hookLogicalId,
|
|
93
|
+
componentInstanceId: ctx.componentInstanceId,
|
|
94
|
+
hookType: 'useMemo',
|
|
95
|
+
depsHash: hash,
|
|
96
|
+
prevDepsHash: prevHashRef.current,
|
|
97
|
+
recomputed: true,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
prevHashRef.current = hash;
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
/** Drop-in `useCallback` with the same accounting as `tracedUseMemo`. */
|
|
106
|
+
export function tracedUseCallback(hookLogicalId, cb, deps) {
|
|
107
|
+
const ctx = useLensmcpContext();
|
|
108
|
+
const hookInstanceId = useStableHookInstance(hookLogicalId);
|
|
109
|
+
const prevHashRef = useRef(undefined);
|
|
110
|
+
const hash = depsHash(deps);
|
|
111
|
+
const recomputed = hash !== prevHashRef.current;
|
|
112
|
+
const memoised = reactUseCallback(cb, deps ?? []);
|
|
113
|
+
if (recomputed) {
|
|
114
|
+
publish({
|
|
115
|
+
source: 'react',
|
|
116
|
+
category: 'render',
|
|
117
|
+
severity: 'debug',
|
|
118
|
+
title: `useCallback recomputed ${hookLogicalId}`,
|
|
119
|
+
fingerprint: `hook-run:${hookLogicalId}`,
|
|
120
|
+
raw: {
|
|
121
|
+
kind: 'hook-run',
|
|
122
|
+
hook: {
|
|
123
|
+
hookInstanceId,
|
|
124
|
+
hookLogicalId,
|
|
125
|
+
componentInstanceId: ctx.componentInstanceId,
|
|
126
|
+
hookType: 'useCallback',
|
|
127
|
+
depsHash: hash,
|
|
128
|
+
prevDepsHash: prevHashRef.current,
|
|
129
|
+
recomputed: true,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
prevHashRef.current = hash;
|
|
135
|
+
return memoised;
|
|
136
|
+
}
|
|
137
|
+
function useStableHookInstance(hookLogicalId) {
|
|
138
|
+
const ref = useRef(undefined);
|
|
139
|
+
if (!ref.current) {
|
|
140
|
+
ref.current = `${hookLogicalId}#${(Math.random() * 1e9) | 0}`;
|
|
141
|
+
}
|
|
142
|
+
return ref.current;
|
|
143
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-shape types for the events `@lensmcp/react-instrumentation`
|
|
3
|
+
* publishes. These mirror the schemas in `@lensmcp/protocol-types`
|
|
4
|
+
* (`RenderAttrs`, `EffectRunAttrs`) but include the identity fields the
|
|
5
|
+
* apps/react reducer needs to attribute renders to specific component
|
|
6
|
+
* instances.
|
|
7
|
+
*/
|
|
8
|
+
import type { RenderPhase, RenderWhy } from '@lensmcp/protocol-types';
|
|
9
|
+
export type { RenderPhase, RenderWhy };
|
|
10
|
+
export interface RenderRecord {
|
|
11
|
+
id: string;
|
|
12
|
+
parentRenderId?: string;
|
|
13
|
+
componentInstanceId: string;
|
|
14
|
+
componentLogicalId: string;
|
|
15
|
+
componentName: string;
|
|
16
|
+
page?: string;
|
|
17
|
+
route?: string;
|
|
18
|
+
rendererId?: string;
|
|
19
|
+
phase: RenderPhase;
|
|
20
|
+
actualDurationMs: number;
|
|
21
|
+
baseDurationMs: number;
|
|
22
|
+
startTime: number;
|
|
23
|
+
commitTime: number;
|
|
24
|
+
why: RenderWhy[];
|
|
25
|
+
}
|
|
26
|
+
export interface EffectRecord {
|
|
27
|
+
hookInstanceId: string;
|
|
28
|
+
hookLogicalId: string;
|
|
29
|
+
componentInstanceId?: string;
|
|
30
|
+
depsHash: string;
|
|
31
|
+
prevDepsHash?: string;
|
|
32
|
+
durationMs: number;
|
|
33
|
+
cleanupRan?: boolean;
|
|
34
|
+
source?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface HookRecord {
|
|
37
|
+
hookInstanceId: string;
|
|
38
|
+
hookLogicalId: string;
|
|
39
|
+
componentInstanceId?: string;
|
|
40
|
+
hookType: 'useMemo' | 'useCallback';
|
|
41
|
+
depsHash: string;
|
|
42
|
+
prevDepsHash?: string;
|
|
43
|
+
recomputed: boolean;
|
|
44
|
+
source?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface SlotChangeRecord {
|
|
47
|
+
slotLogicalId: string;
|
|
48
|
+
slotInstanceId: string;
|
|
49
|
+
fromComponent?: string;
|
|
50
|
+
toComponent?: string;
|
|
51
|
+
componentInstanceId?: string;
|
|
52
|
+
source?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ComponentMountRecord {
|
|
55
|
+
logicalId: string;
|
|
56
|
+
instanceId: string;
|
|
57
|
+
generation: number;
|
|
58
|
+
name: string;
|
|
59
|
+
source?: string;
|
|
60
|
+
page?: string;
|
|
61
|
+
route?: string;
|
|
62
|
+
}
|
|
63
|
+
/** Anything the lib publishes downstream of the client-runtime. */
|
|
64
|
+
export type ReactPublishPayload = {
|
|
65
|
+
kind: 'render';
|
|
66
|
+
render: RenderRecord;
|
|
67
|
+
} | {
|
|
68
|
+
kind: 'effect-run';
|
|
69
|
+
effect: EffectRecord;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'hook-run';
|
|
72
|
+
hook: HookRecord;
|
|
73
|
+
} | {
|
|
74
|
+
kind: 'slot-content-changed';
|
|
75
|
+
slot: SlotChangeRecord;
|
|
76
|
+
} | {
|
|
77
|
+
kind: 'component-mount';
|
|
78
|
+
component: ComponentMountRecord;
|
|
79
|
+
} | {
|
|
80
|
+
kind: 'component-unmount';
|
|
81
|
+
component: ComponentMountRecord;
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEtE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;AAEvC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,WAAW,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,SAAS,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,SAAS,GAAG,aAAa,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,mEAAmE;AACnE,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,IAAI,EAAE,gBAAgB,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,oBAAoB,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,SAAS,EAAE,oBAAoB,CAAA;CAAE,CAAC"}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lensmcp/react-instrumentation",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"module": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"import": "./index.js",
|
|
13
|
+
"default": "./index.js"
|
|
14
|
+
},
|
|
15
|
+
"./babel-plugin": {
|
|
16
|
+
"types": "./lib/babel-plugin.d.ts",
|
|
17
|
+
"import": "./lib/babel-plugin.js",
|
|
18
|
+
"default": "./lib/babel-plugin.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@lensmcp/protocol-types": "1.0.0",
|
|
23
|
+
"tslib": "^2.3.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@babel/core": "^7.0.0",
|
|
27
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@babel/core": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/babel__core": "^7.0.0",
|
|
36
|
+
"@types/react": "^19.0.0"
|
|
37
|
+
},
|
|
38
|
+
"license": "Apache-2.0",
|
|
39
|
+
"homepage": "https://github.com/kiwiapps-ltd/lensmcp#readme",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/kiwiapps-ltd/lensmcp/issues"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"description": "Zero-config React instrumentation for LensMCP (Babel transform + flow/render runtime).",
|
|
50
|
+
"keywords": [
|
|
51
|
+
"lensmcp",
|
|
52
|
+
"mcp",
|
|
53
|
+
"observability",
|
|
54
|
+
"ai-agents",
|
|
55
|
+
"claude",
|
|
56
|
+
"react",
|
|
57
|
+
"babel"
|
|
58
|
+
],
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/kiwiapps-ltd/lensmcp.git",
|
|
62
|
+
"directory": "libs/react-instrumentation"
|
|
63
|
+
}
|
|
64
|
+
}
|