@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/context.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface LensmcpIdentityContext {
|
|
2
|
+
rendererId: string;
|
|
3
|
+
page?: string;
|
|
4
|
+
route?: string;
|
|
5
|
+
/** Optional override — when set, hook helpers tag their events with
|
|
6
|
+
* this component instance instead of falling back to a synthetic id. */
|
|
7
|
+
componentInstanceId?: string;
|
|
8
|
+
/** Stable component logicalId (file + name), if known. */
|
|
9
|
+
componentLogicalId?: string;
|
|
10
|
+
/** Optional parent render id (set by LensmcpRoot's Profiler onRender). */
|
|
11
|
+
lastRenderId?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const LensmcpContext: import("react").Context<LensmcpIdentityContext>;
|
|
14
|
+
export declare function useLensmcpContext(): LensmcpIdentityContext;
|
|
15
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/lib/context.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;6EACyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAID,eAAO,MAAM,cAAc,iDAAoD,CAAC;AAEhF,wBAAgB,iBAAiB,IAAI,sBAAsB,CAE1D"}
|
package/lib/context.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface FlowFetchOptions {
|
|
2
|
+
/** Header carrying the flow id. Default `x-lensmcp-flow-id`. */
|
|
3
|
+
flowHeader?: string;
|
|
4
|
+
/** Header carrying the origin node id. Default `x-lensmcp-origin-node-id`. */
|
|
5
|
+
originHeader?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function installFlowFetch(options?: FlowFetchOptions): () => void;
|
|
8
|
+
//# sourceMappingURL=flow-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-fetch.d.ts","sourceRoot":"","sources":["../../src/lib/flow-fetch.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAWD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,gBAAqB,GAAG,MAAM,IAAI,CA2C3E"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow-header propagation across the network boundary. When a request is
|
|
3
|
+
* made while a flow is active (inside `runInFlow`/`withFlow`), inject
|
|
4
|
+
* `x-lensmcp-flow-id` + `x-lensmcp-origin-node-id` headers so the backend
|
|
5
|
+
* (`TraceInterceptor` in `@lensmcp/nest-instrumentation`) stamps its
|
|
6
|
+
* server-request + span + db-query events with the SAME flowId. That is
|
|
7
|
+
* what lets `story.compile` and `graph.*` correlate a single user action
|
|
8
|
+
* all the way from the click to the DB query.
|
|
9
|
+
*
|
|
10
|
+
* Works in the browser and in Node (both have a global `fetch`). The
|
|
11
|
+
* header is read at *call time* (synchronous), so a `fetch()` invoked
|
|
12
|
+
* inside the synchronous portion of a flow is tagged even though the
|
|
13
|
+
* awaited continuation runs after the flow stack unwinds.
|
|
14
|
+
*/
|
|
15
|
+
import { activeFlow, beginBackgroundFlow, extendFlowWindow, publish } from './publish.js';
|
|
16
|
+
/**
|
|
17
|
+
* Monkeypatch `globalThis.fetch` to inject flow headers. Returns an
|
|
18
|
+
* uninstall function that restores the original. No-op (returns a no-op
|
|
19
|
+
* uninstaller) if there's no global fetch.
|
|
20
|
+
*/
|
|
21
|
+
let pageLoadFlowStarted = false;
|
|
22
|
+
export function installFlowFetch(options = {}) {
|
|
23
|
+
const g = globalThis;
|
|
24
|
+
if (typeof g.fetch !== 'function')
|
|
25
|
+
return () => undefined;
|
|
26
|
+
// installFlowFetch runs at ENTRY-MODULE eval (the babel transform injects
|
|
27
|
+
// it before the root render) — earlier than any mount effect. That makes
|
|
28
|
+
// it the one reliable place to declare the PAGE LOAD itself a flow, so the
|
|
29
|
+
// boot-time fetches (/me, list queries…) and the renders they trigger all
|
|
30
|
+
// stitch into one trace. A refresh without this produced NO flow at all.
|
|
31
|
+
if (!pageLoadFlowStarted && typeof location !== 'undefined' && typeof document !== 'undefined') {
|
|
32
|
+
pageLoadFlowStarted = true;
|
|
33
|
+
beginBackgroundFlow({ originType: 'page-load', originNodeId: `page:${location.pathname}` });
|
|
34
|
+
}
|
|
35
|
+
const flowHeader = options.flowHeader ?? 'x-lensmcp-flow-id';
|
|
36
|
+
const originHeader = options.originHeader ?? 'x-lensmcp-origin-node-id';
|
|
37
|
+
const original = g.fetch.bind(globalThis);
|
|
38
|
+
const patched = ((input, init) => {
|
|
39
|
+
// Sync stack (a wrapped handler) OR the causality window (page-load,
|
|
40
|
+
// post-response continuations) — both mean this fetch belongs to a flow.
|
|
41
|
+
const flow = activeFlow();
|
|
42
|
+
if (!flow?.flowId)
|
|
43
|
+
return original(input, init);
|
|
44
|
+
// Merge onto any caller-supplied headers (+ a Request's own headers).
|
|
45
|
+
const requestHeaders = typeof input === 'object' && input !== null && 'headers' in input
|
|
46
|
+
? input.headers
|
|
47
|
+
: undefined;
|
|
48
|
+
const headers = new Headers(init?.headers ?? requestHeaders ?? undefined);
|
|
49
|
+
if (!headers.has(flowHeader))
|
|
50
|
+
headers.set(flowHeader, flow.flowId);
|
|
51
|
+
if (flow.originNodeId && !headers.has(originHeader)) {
|
|
52
|
+
headers.set(originHeader, flow.originNodeId);
|
|
53
|
+
}
|
|
54
|
+
const result = original(input, { ...init, headers });
|
|
55
|
+
publishFetchLeg(flow, input, init, result);
|
|
56
|
+
return result;
|
|
57
|
+
});
|
|
58
|
+
g.fetch = patched;
|
|
59
|
+
return () => {
|
|
60
|
+
g.fetch = original;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Publish the flow's network leg: a start event (synchronous — stamped from
|
|
65
|
+
* the flow stack) and a completion event (asynchronous — the flow has
|
|
66
|
+
* unwound by then, so its context is attached explicitly). Together with
|
|
67
|
+
* the backend's server-request event (same flowId via the header) this is
|
|
68
|
+
* what lets a story read click → fetch → server → DB.
|
|
69
|
+
*/
|
|
70
|
+
function publishFetchLeg(flow, input, init, result) {
|
|
71
|
+
const url = typeof input === 'string'
|
|
72
|
+
? input
|
|
73
|
+
: input instanceof URL
|
|
74
|
+
? input.href
|
|
75
|
+
: (input.url ?? String(input));
|
|
76
|
+
const method = init?.method ??
|
|
77
|
+
(typeof input === 'object' && input !== null && 'method' in input
|
|
78
|
+
? (input.method ?? 'GET')
|
|
79
|
+
: 'GET');
|
|
80
|
+
const startedAt = Date.now();
|
|
81
|
+
const flowContext = {
|
|
82
|
+
flowId: flow.flowId,
|
|
83
|
+
...(flow.originType ? { originType: flow.originType } : {}),
|
|
84
|
+
...(flow.originNodeId ? { originNodeId: flow.originNodeId } : {}),
|
|
85
|
+
};
|
|
86
|
+
publish({
|
|
87
|
+
source: 'react',
|
|
88
|
+
category: 'network',
|
|
89
|
+
severity: 'info',
|
|
90
|
+
title: `fetch ${method} ${url}`,
|
|
91
|
+
fingerprint: `flow-fetch:${method}:${url}`,
|
|
92
|
+
raw: { kind: 'fetch-start', url, method },
|
|
93
|
+
});
|
|
94
|
+
// Observe on a branch — never alter the caller's promise chain.
|
|
95
|
+
void result.then((res) => {
|
|
96
|
+
// Re-open the causality window: the state update + re-render that
|
|
97
|
+
// consume this response land in the next ticks and belong to this flow.
|
|
98
|
+
extendFlowWindow(flow);
|
|
99
|
+
publish({
|
|
100
|
+
source: 'react',
|
|
101
|
+
category: 'network',
|
|
102
|
+
severity: res.ok ? 'info' : 'warning',
|
|
103
|
+
title: `fetch ${method} ${url} → ${res.status}`,
|
|
104
|
+
fingerprint: `flow-fetch:${method}:${url}`,
|
|
105
|
+
context: flowContext,
|
|
106
|
+
raw: { kind: 'fetch-done', url, method, status: res.status, ok: res.ok, startedAt, durationMs: Date.now() - startedAt },
|
|
107
|
+
});
|
|
108
|
+
}, (err) => {
|
|
109
|
+
extendFlowWindow(flow);
|
|
110
|
+
publish({
|
|
111
|
+
source: 'react',
|
|
112
|
+
category: 'network',
|
|
113
|
+
severity: 'error',
|
|
114
|
+
title: `fetch ${method} ${url} failed`,
|
|
115
|
+
message: err instanceof Error ? err.message : String(err),
|
|
116
|
+
fingerprint: `flow-fetch:${method}:${url}`,
|
|
117
|
+
context: flowContext,
|
|
118
|
+
raw: { kind: 'fetch-error', url, method, startedAt, durationMs: Date.now() - startedAt },
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
package/lib/hoc.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ComponentType } from 'react';
|
|
2
|
+
export interface WithTraceOptions {
|
|
3
|
+
componentName?: string;
|
|
4
|
+
/** Source file path — set this for projects that can't enable the JSX transform. */
|
|
5
|
+
source?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `withTraceComponent(MyComponent)` — opt-in tracer for projects that
|
|
9
|
+
* can't enable the global Babel transform. Wraps the component in a
|
|
10
|
+
* Profiler and emits a `render` event on every commit.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withTraceComponent<P extends object>(Wrapped: ComponentType<P>, opts?: WithTraceOptions): ComponentType<P>;
|
|
13
|
+
//# sourceMappingURL=hoc.d.ts.map
|
package/lib/hoc.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hoc.d.ts","sourceRoot":"","sources":["../../src/lib/hoc.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAGnB,MAAM,OAAO,CAAC;AAKf,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EACjD,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EACzB,IAAI,GAAE,gBAAqB,GAC1B,aAAa,CAAC,CAAC,CAAC,CA+ClB"}
|
package/lib/hoc.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Profiler, } from 'react';
|
|
3
|
+
import { publish } from './publish.js';
|
|
4
|
+
import { makeInstanceId } from './identity.js';
|
|
5
|
+
/**
|
|
6
|
+
* `withTraceComponent(MyComponent)` — opt-in tracer for projects that
|
|
7
|
+
* can't enable the global Babel transform. Wraps the component in a
|
|
8
|
+
* Profiler and emits a `render` event on every commit.
|
|
9
|
+
*/
|
|
10
|
+
export function withTraceComponent(Wrapped, opts = {}) {
|
|
11
|
+
const name = opts.componentName ?? Wrapped.displayName ?? Wrapped.name ?? 'TracedComponent';
|
|
12
|
+
const file = opts.source ?? '(unknown)';
|
|
13
|
+
const logicalId = `react:component:${file}:${name}`;
|
|
14
|
+
function Traced(props) {
|
|
15
|
+
let lastRenderId;
|
|
16
|
+
const onRender = (_id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
|
17
|
+
const { instanceId } = makeInstanceId(logicalId);
|
|
18
|
+
const record = {
|
|
19
|
+
id: instanceId,
|
|
20
|
+
parentRenderId: lastRenderId,
|
|
21
|
+
componentInstanceId: instanceId,
|
|
22
|
+
componentLogicalId: logicalId,
|
|
23
|
+
componentName: name,
|
|
24
|
+
phase: phase === 'mount' ? 'mount' : 'update',
|
|
25
|
+
actualDurationMs: actualDuration,
|
|
26
|
+
baseDurationMs: baseDuration,
|
|
27
|
+
startTime,
|
|
28
|
+
commitTime,
|
|
29
|
+
why: [{ type: 'force', reason: 'withTraceComponent' }],
|
|
30
|
+
};
|
|
31
|
+
lastRenderId = instanceId;
|
|
32
|
+
publish({
|
|
33
|
+
source: 'react',
|
|
34
|
+
category: 'render',
|
|
35
|
+
severity: 'info',
|
|
36
|
+
title: `render ${name} (${phase})`,
|
|
37
|
+
fingerprint: `react-render:${logicalId}`,
|
|
38
|
+
raw: { kind: 'render', render: record },
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
return (_jsx(Profiler, { id: logicalId, onRender: onRender, children: _jsx(Wrapped, { ...props }) }));
|
|
42
|
+
}
|
|
43
|
+
Traced.displayName = `Traced(${name})`;
|
|
44
|
+
return Traced;
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity helpers — produce stable `logicalId` and per-mount
|
|
3
|
+
* `instanceId` values for components, hooks, and slots. Mirrors the
|
|
4
|
+
* shapes documented in `planning/02-data-model.md#logicalid`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function nextGeneration(logicalId: string): number;
|
|
7
|
+
export declare function shortHash(): string;
|
|
8
|
+
export declare function componentLogicalId(file: string, name: string): string;
|
|
9
|
+
export declare function hookLogicalId(file: string, componentName: string, hookName: string, index: number): string;
|
|
10
|
+
export declare function slotLogicalId(file: string, line: number, kind: string): string;
|
|
11
|
+
export declare function makeInstanceId(logicalId: string): {
|
|
12
|
+
instanceId: string;
|
|
13
|
+
generation: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Cheap deterministic hash over an array of dependency values. Used by
|
|
17
|
+
* the traced hooks to compute `depsHash` so the reducer can decide
|
|
18
|
+
* whether a hook re-ran because of a dep change.
|
|
19
|
+
*/
|
|
20
|
+
export declare function depsHash(deps: readonly unknown[] | undefined): string;
|
|
21
|
+
/** Test hook to reset generation counters between smoke iterations. */
|
|
22
|
+
export declare function resetIdentityState(): void;
|
|
23
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/lib/identity.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAgB,SAAS,IAAI,MAAM,CAGlC;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAErE;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,MAAM,CAER;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAMA;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,GAAG,MAAM,CAUrE;AAoBD,uEAAuE;AACvE,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC"}
|
package/lib/identity.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity helpers — produce stable `logicalId` and per-mount
|
|
3
|
+
* `instanceId` values for components, hooks, and slots. Mirrors the
|
|
4
|
+
* shapes documented in `planning/02-data-model.md#logicalid`.
|
|
5
|
+
*/
|
|
6
|
+
const generations = new Map();
|
|
7
|
+
let entropy = 0;
|
|
8
|
+
export function nextGeneration(logicalId) {
|
|
9
|
+
const g = (generations.get(logicalId) ?? 0) + 1;
|
|
10
|
+
generations.set(logicalId, g);
|
|
11
|
+
return g;
|
|
12
|
+
}
|
|
13
|
+
export function shortHash() {
|
|
14
|
+
entropy = (entropy + 1) % 0xffffffff;
|
|
15
|
+
return ((Date.now() & 0xfff) ^ entropy).toString(36).slice(-4);
|
|
16
|
+
}
|
|
17
|
+
export function componentLogicalId(file, name) {
|
|
18
|
+
return `react:component:${file}:${name}`;
|
|
19
|
+
}
|
|
20
|
+
export function hookLogicalId(file, componentName, hookName, index) {
|
|
21
|
+
return `react:hook:${file}:${componentName}:${hookName}:${index}`;
|
|
22
|
+
}
|
|
23
|
+
export function slotLogicalId(file, line, kind) {
|
|
24
|
+
return `react:slot:${file}:${line}:${kind}`;
|
|
25
|
+
}
|
|
26
|
+
export function makeInstanceId(logicalId) {
|
|
27
|
+
const generation = nextGeneration(logicalId);
|
|
28
|
+
return {
|
|
29
|
+
instanceId: `${logicalId}#${generation}#${shortHash()}`,
|
|
30
|
+
generation,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Cheap deterministic hash over an array of dependency values. Used by
|
|
35
|
+
* the traced hooks to compute `depsHash` so the reducer can decide
|
|
36
|
+
* whether a hook re-ran because of a dep change.
|
|
37
|
+
*/
|
|
38
|
+
export function depsHash(deps) {
|
|
39
|
+
if (!deps)
|
|
40
|
+
return 'no-deps';
|
|
41
|
+
let h = 5381;
|
|
42
|
+
for (const dep of deps) {
|
|
43
|
+
const repr = canonical(dep);
|
|
44
|
+
for (let i = 0; i < repr.length; i++) {
|
|
45
|
+
h = (h * 33) ^ repr.charCodeAt(i);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return (h >>> 0).toString(36);
|
|
49
|
+
}
|
|
50
|
+
function canonical(v) {
|
|
51
|
+
if (v === null)
|
|
52
|
+
return 'null';
|
|
53
|
+
if (v === undefined)
|
|
54
|
+
return 'undefined';
|
|
55
|
+
const t = typeof v;
|
|
56
|
+
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint') {
|
|
57
|
+
return `${t}:${String(v)}`;
|
|
58
|
+
}
|
|
59
|
+
if (t === 'function') {
|
|
60
|
+
const name = v.name ?? 'anonymous';
|
|
61
|
+
return `fn:${name}:${v.toString().length}`;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return `obj:${JSON.stringify(v)}`;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return `obj:circular`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Test hook to reset generation counters between smoke iterations. */
|
|
71
|
+
export function resetIdentityState() {
|
|
72
|
+
generations.clear();
|
|
73
|
+
entropy = 0;
|
|
74
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface LensmcpRootProps {
|
|
3
|
+
/** Stable id for this renderer (defaults to "lensmcp-root"). */
|
|
4
|
+
rendererId?: string;
|
|
5
|
+
/** Logical page/route label. The reducer uses this to group renders. */
|
|
6
|
+
page?: string;
|
|
7
|
+
route?: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Wraps the React tree in a `<Profiler>` and a context provider so the
|
|
12
|
+
* hook helpers can tag their events with the right renderer / page /
|
|
13
|
+
* route. Drop-in:
|
|
14
|
+
*
|
|
15
|
+
* ReactDOM.createRoot(el).render(
|
|
16
|
+
* <LensmcpRoot page="home" route="/">
|
|
17
|
+
* <App />
|
|
18
|
+
* </LensmcpRoot>
|
|
19
|
+
* );
|
|
20
|
+
*/
|
|
21
|
+
export declare function LensmcpRoot(props: LensmcpRootProps): ReactNode;
|
|
22
|
+
//# sourceMappingURL=lensmcp-root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lensmcp-root.d.ts","sourceRoot":"","sources":["../../src/lib/lensmcp-root.tsx"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EAGf,MAAM,OAAO,CAAC;AAMf,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,SAAS,CAuD9D"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Profiler, useMemo, useRef, } from 'react';
|
|
3
|
+
import { LensmcpContext } from './context.js';
|
|
4
|
+
import { publish } from './publish.js';
|
|
5
|
+
import { makeInstanceId } from './identity.js';
|
|
6
|
+
/**
|
|
7
|
+
* Wraps the React tree in a `<Profiler>` and a context provider so the
|
|
8
|
+
* hook helpers can tag their events with the right renderer / page /
|
|
9
|
+
* route. Drop-in:
|
|
10
|
+
*
|
|
11
|
+
* ReactDOM.createRoot(el).render(
|
|
12
|
+
* <LensmcpRoot page="home" route="/">
|
|
13
|
+
* <App />
|
|
14
|
+
* </LensmcpRoot>
|
|
15
|
+
* );
|
|
16
|
+
*/
|
|
17
|
+
export function LensmcpRoot(props) {
|
|
18
|
+
const rendererId = props.rendererId ?? 'lensmcp-root';
|
|
19
|
+
const ctxValue = useMemo(() => ({ rendererId, page: props.page, route: props.route }), [rendererId, props.page, props.route]);
|
|
20
|
+
const renderIdsRef = useRef(new Map());
|
|
21
|
+
const lastRenderIdRef = useRef(undefined);
|
|
22
|
+
const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
|
23
|
+
const logicalId = `render:${rendererId}:${id}`;
|
|
24
|
+
const { instanceId } = makeInstanceId(logicalId);
|
|
25
|
+
const renderRecord = {
|
|
26
|
+
id: instanceId,
|
|
27
|
+
parentRenderId: lastRenderIdRef.current,
|
|
28
|
+
componentInstanceId: instanceId,
|
|
29
|
+
componentLogicalId: logicalId,
|
|
30
|
+
componentName: id,
|
|
31
|
+
page: props.page,
|
|
32
|
+
route: props.route,
|
|
33
|
+
rendererId,
|
|
34
|
+
phase: phase === 'mount' ? 'mount' : 'update',
|
|
35
|
+
actualDurationMs: actualDuration,
|
|
36
|
+
baseDurationMs: baseDuration,
|
|
37
|
+
startTime,
|
|
38
|
+
commitTime,
|
|
39
|
+
why: [{ type: 'force', reason: 'profiler-onrender' }],
|
|
40
|
+
};
|
|
41
|
+
lastRenderIdRef.current = instanceId;
|
|
42
|
+
renderIdsRef.current.set(id, instanceId);
|
|
43
|
+
publish({
|
|
44
|
+
source: 'react',
|
|
45
|
+
category: 'render',
|
|
46
|
+
severity: 'info',
|
|
47
|
+
title: `render ${id} (${phase})`,
|
|
48
|
+
fingerprint: `react-render:${logicalId}`,
|
|
49
|
+
raw: { kind: 'render', render: renderRecord },
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
return (_jsx(LensmcpContext.Provider, { value: ctxValue, children: _jsx(Profiler, { id: rendererId, onRender: onRender, children: props.children }) }));
|
|
53
|
+
}
|
package/lib/publish.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single publish channel + module-level "current flow" stack. The
|
|
3
|
+
* `@lensmcp/client-runtime` injected into the served HTML sets
|
|
4
|
+
* `window.__LENSMCP_PUBLISH__` to a function that takes a partial event
|
|
5
|
+
* and forwards it to the LensMCP bus (via WebSocket in dev, or via the
|
|
6
|
+
* FrontMCP server transport in production).
|
|
7
|
+
*
|
|
8
|
+
* If the channel isn't installed we silently no-op — this keeps the
|
|
9
|
+
* library safe to import from any React tree (tests, SSR, etc).
|
|
10
|
+
*/
|
|
11
|
+
import type { ReactPublishPayload } from './types.js';
|
|
12
|
+
export type PublishCategory = 'render' | 'state' | 'runtime' | 'network' | 'visual' | 'trace';
|
|
13
|
+
export interface FlowSpec {
|
|
14
|
+
flowId: string;
|
|
15
|
+
originType?: string;
|
|
16
|
+
originNodeId?: string;
|
|
17
|
+
causedByNodeId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PublishEnvelope {
|
|
20
|
+
source: 'react' | 'valtio' | 'client-runtime' | 'visual';
|
|
21
|
+
category: PublishCategory;
|
|
22
|
+
severity?: 'debug' | 'info' | 'warning' | 'error';
|
|
23
|
+
title: string;
|
|
24
|
+
message?: string;
|
|
25
|
+
fingerprint?: string;
|
|
26
|
+
/** Optional context overrides; auto-stamped flow context is merged underneath. */
|
|
27
|
+
context?: Partial<{
|
|
28
|
+
flowId: string;
|
|
29
|
+
originType: string;
|
|
30
|
+
originNodeId: string;
|
|
31
|
+
causedByNodeId: string;
|
|
32
|
+
route: string;
|
|
33
|
+
url: string;
|
|
34
|
+
}>;
|
|
35
|
+
raw: ReactPublishPayload | Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
type PublishFn = (event: PublishEnvelope) => void;
|
|
38
|
+
declare global {
|
|
39
|
+
var __LENSMCP_PUBLISH__: PublishFn | undefined;
|
|
40
|
+
}
|
|
41
|
+
export declare function extendFlowWindow(spec?: FlowSpec): void;
|
|
42
|
+
/** The flow that owns work happening NOW: sync stack first, else the window. */
|
|
43
|
+
export declare function activeFlow(): FlowSpec | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Start a flow with no synchronous handler to wrap — page loads, timers,
|
|
46
|
+
* websocket pushes. Opens the causality window (so the async work that
|
|
47
|
+
* follows inherits the flow) and publishes the origin event that makes the
|
|
48
|
+
* flow exist in the reducer.
|
|
49
|
+
*/
|
|
50
|
+
export declare function beginBackgroundFlow(origin: {
|
|
51
|
+
originType: string;
|
|
52
|
+
originNodeId?: string;
|
|
53
|
+
}): FlowSpec;
|
|
54
|
+
export declare function setPublisher(fn: PublishFn | undefined): void;
|
|
55
|
+
/**
|
|
56
|
+
* Push a flow onto the module-level stack for the duration of `fn`. Any
|
|
57
|
+
* `publish()` calls inside `fn` (including transitively triggered
|
|
58
|
+
* Valtio subscribers and React Profiler callbacks that fire synchronously)
|
|
59
|
+
* will inherit the flow context.
|
|
60
|
+
*
|
|
61
|
+
* NB: only synchronous propagation. For async chains (`fetch().then`,
|
|
62
|
+
* `setTimeout`, promises) the caller must re-enter the flow with
|
|
63
|
+
* `runInFlow` at the continuation site. The Phase 6.5 carryover adds
|
|
64
|
+
* automatic continuation tracking via an async-context shim.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runInFlow<T>(spec: FlowSpec, fn: () => T): T;
|
|
67
|
+
export declare function currentFlow(): FlowSpec | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Wrap a React event handler so every call runs inside a fresh flow.
|
|
70
|
+
* The flowId is generated per invocation; `originType` defaults to
|
|
71
|
+
* `'user-click'`. Pass `originNodeId` from the component identity
|
|
72
|
+
* (typically the `data-agent-component` value).
|
|
73
|
+
*
|
|
74
|
+
* The babel transform wraps `on*` JSX values blindly, so this must be
|
|
75
|
+
* total: non-function values (e.g. `onClick={maybeUndefined}`) pass
|
|
76
|
+
* through unchanged, and an already-flowed handler is returned as-is —
|
|
77
|
+
* layered components forwarding the same prop would otherwise nest a
|
|
78
|
+
* flow per layer and crash on the inner non-function (`handler is not
|
|
79
|
+
* a function`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function withFlow<TArgs extends unknown[], TResult>(handler: (...args: TArgs) => TResult, spec?: {
|
|
82
|
+
originType?: string;
|
|
83
|
+
originNodeId?: string;
|
|
84
|
+
}): (...args: TArgs) => TResult;
|
|
85
|
+
export declare function publish(env: PublishEnvelope): void;
|
|
86
|
+
/** Test hook — only useful in unit tests / smokes. */
|
|
87
|
+
export declare function drainQueue(): PublishEnvelope[];
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=publish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/lib/publish.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,MAAM,eAAe,GACvB,QAAQ,GACR,OAAO,GACP,SAAS,GACT,SAAS,GACT,QAAQ,GACR,OAAO,CAAC;AAEZ,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,gBAAgB,GAAG,QAAQ,CAAC;IACzD,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,OAAO,CAAC,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC,CAAC;IACH,GAAG,EAAE,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpD;AAED,KAAK,SAAS,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;AAElD,OAAO,CAAC,MAAM,CAAC;IACb,IAAI,mBAAmB,EAAE,SAAS,GAAG,SAAS,CAAC;CAChD;AAmBD,wBAAgB,gBAAgB,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAGtD;AAMD,gFAAgF;AAChF,wBAAgB,UAAU,IAAI,QAAQ,GAAG,SAAS,CAEjD;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,QAAQ,CAgBnG;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,GAAG,IAAI,CAc5D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQ3D;AAED,wBAAgB,WAAW,IAAI,QAAQ,GAAG,SAAS,CAElD;AAKD;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EACvD,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,EACpC,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO,GACxD,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAgC7B;AAQD,wBAAgB,OAAO,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CA6BlD;AAED,sDAAsD;AACtD,wBAAgB,UAAU,IAAI,eAAe,EAAE,CAE9C"}
|