@lensmcp/core 1.18.4 → 1.18.7
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/index.js +1 -11
- package/lib/common.js +1 -1
- package/lib/event-bus.js +1 -65
- package/lib/fingerprint.js +1 -24
- package/lib/graph-from-events.js +1 -141
- package/lib/graph-store.js +1 -171
- package/lib/patterns.js +1 -163
- package/lib/redact.d.ts +127 -0
- package/lib/redact.js +1 -0
- package/lib/reducer-registry.js +1 -28
- package/lib/resource-store.js +1 -71
- package/lib/story.js +1 -199
- package/lib/thresholds.js +1 -23
- package/lib/ulid.js +1 -32
- package/package.json +7 -2
package/index.js
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export * from './lib/ulid.js';
|
|
3
|
-
export * from './lib/event-bus.js';
|
|
4
|
-
export * from './lib/resource-store.js';
|
|
5
|
-
export * from './lib/graph-store.js';
|
|
6
|
-
export * from './lib/fingerprint.js';
|
|
7
|
-
export * from './lib/reducer-registry.js';
|
|
8
|
-
export * from './lib/story.js';
|
|
9
|
-
export * from './lib/patterns.js';
|
|
10
|
-
export * from './lib/thresholds.js';
|
|
11
|
-
export * from './lib/graph-from-events.js';
|
|
1
|
+
"use strict";export*from"./lib/common.js";export*from"./lib/ulid.js";export*from"./lib/event-bus.js";export*from"./lib/resource-store.js";export*from"./lib/graph-store.js";export*from"./lib/fingerprint.js";export*from"./lib/reducer-registry.js";export*from"./lib/story.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";export*from"./lib/graph-from-events.js";
|
package/lib/common.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
"use strict";export{};
|
package/lib/event-bus.js
CHANGED
|
@@ -1,65 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* In-process, single-threaded, bounded event bus. Subscribers are notified
|
|
3
|
-
* synchronously (errors from one handler do not stop others). Backpressure
|
|
4
|
-
* is enforced by a fixed ring buffer of buffered events used only when
|
|
5
|
-
* deferred delivery is configured — by default we deliver immediately.
|
|
6
|
-
*
|
|
7
|
-
* The bus is intentionally tiny. Reducers run on top of it.
|
|
8
|
-
*/
|
|
9
|
-
export class EventBus {
|
|
10
|
-
constructor(opts = {}) {
|
|
11
|
-
this.subs = new Set();
|
|
12
|
-
this.dropped = 0;
|
|
13
|
-
this.capacity = opts.capacity ?? 10_000;
|
|
14
|
-
this.onDrop = opts.onDrop;
|
|
15
|
-
this.transform = opts.transform;
|
|
16
|
-
}
|
|
17
|
-
publish(event) {
|
|
18
|
-
if (this.subs.size > this.capacity) {
|
|
19
|
-
this.dropped++;
|
|
20
|
-
this.onDrop?.(1);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
let delivered = event;
|
|
24
|
-
if (this.transform) {
|
|
25
|
-
try {
|
|
26
|
-
delivered = this.transform(event) ?? event;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// A misbehaving transform must never swallow an event: fall back to
|
|
30
|
-
// the original rather than dropping observability on the floor.
|
|
31
|
-
delivered = event;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
for (const sub of this.subs) {
|
|
35
|
-
if (sub.filter && !sub.filter(delivered))
|
|
36
|
-
continue;
|
|
37
|
-
try {
|
|
38
|
-
const ret = sub.handler(delivered);
|
|
39
|
-
if (ret && typeof ret.then === 'function') {
|
|
40
|
-
ret.catch(() => {
|
|
41
|
-
/* swallow async handler errors; reducers must not affect bus */
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
/* swallow sync handler errors */
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
subscribe(handler, filter) {
|
|
51
|
-
const sub = { handler, filter };
|
|
52
|
-
this.subs.add(sub);
|
|
53
|
-
return () => {
|
|
54
|
-
this.subs.delete(sub);
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
/** Number of dropped events since construction. */
|
|
58
|
-
droppedCount() {
|
|
59
|
-
return this.dropped;
|
|
60
|
-
}
|
|
61
|
-
/** Number of currently registered subscribers. */
|
|
62
|
-
subscriberCount() {
|
|
63
|
-
return this.subs.size;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
1
|
+
"use strict";var o=Object.defineProperty;var n=(e,t)=>o(e,"name",{value:t,configurable:!0});var c=Object.defineProperty,h=n((e,t)=>c(e,"name",{value:t,configurable:!0}),"e");export class EventBus{static{n(this,"EventBus")}static{h(this,"EventBus")}constructor(t={}){this.subs=new Set,this.dropped=0,this.capacity=t.capacity??1e4,this.onDrop=t.onDrop,this.transform=t.transform}publish(t){if(this.subs.size>this.capacity){this.dropped++,this.onDrop?.(1);return}let s=t;if(this.transform)try{s=this.transform(t)??t}catch{s=t}for(const r of this.subs)if(!(r.filter&&!r.filter(s)))try{const i=r.handler(s);i&&typeof i.then=="function"&&i.catch(()=>{})}catch{}}subscribe(t,s){const r={handler:t,filter:s};return this.subs.add(r),()=>{this.subs.delete(r)}}droppedCount(){return this.dropped}subscriberCount(){return this.subs.size}}
|
package/lib/fingerprint.js
CHANGED
|
@@ -1,24 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Stable, deterministic fingerprints for events. Same root cause →
|
|
3
|
-
* same fingerprint, even if line numbers or stack traces drift between
|
|
4
|
-
* runs. See `02-data-model.md#fingerprinting`.
|
|
5
|
-
*
|
|
6
|
-
* We deliberately do not use a strong hash — fingerprints are routing
|
|
7
|
-
* keys, not security tokens. A lightweight FNV-1a 32-bit hash is enough
|
|
8
|
-
* to dedupe noisy event streams.
|
|
9
|
-
*/
|
|
10
|
-
const FNV_OFFSET_BASIS = 0x811c9dc5;
|
|
11
|
-
const FNV_PRIME = 0x01000193;
|
|
12
|
-
function fnv1a32(input) {
|
|
13
|
-
let h = FNV_OFFSET_BASIS;
|
|
14
|
-
for (let i = 0; i < input.length; i++) {
|
|
15
|
-
h ^= input.charCodeAt(i);
|
|
16
|
-
h = Math.imul(h, FNV_PRIME);
|
|
17
|
-
}
|
|
18
|
-
// Force to unsigned 32-bit and emit zero-padded hex.
|
|
19
|
-
return (h >>> 0).toString(16).padStart(8, '0');
|
|
20
|
-
}
|
|
21
|
-
export function fingerprint(input) {
|
|
22
|
-
const parts = [input.kind, input.identity, input.file ?? '', input.detail ?? ''];
|
|
23
|
-
return `${input.kind}:${fnv1a32(parts.join('|'))}`;
|
|
24
|
-
}
|
|
1
|
+
"use strict";var a=Object.defineProperty;var r=(n,e)=>a(n,"name",{value:e,configurable:!0});var f=Object.defineProperty,i=r((n,e)=>f(n,"name",{value:e,configurable:!0}),"e");const l=2166136261,u=16777619;function o(n){let e=2166136261;for(let t=0;t<n.length;t++)e^=n.charCodeAt(t),e=Math.imul(e,16777619);return(e>>>0).toString(16).padStart(8,"0")}r(o,"i"),i(o,"fnv1a32");export function fingerprint(n){const e=[n.kind,n.identity,n.file??"",n.detail??""];return`${n.kind}:${o(e.join("|"))}`}r(fingerprint,"fingerprint"),i(fingerprint,"fingerprint");
|
package/lib/graph-from-events.js
CHANGED
|
@@ -1,141 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Convert one event into a graph node, or return undefined if the event
|
|
3
|
-
* doesn't promote to a node.
|
|
4
|
-
*/
|
|
5
|
-
function eventToNode(event) {
|
|
6
|
-
const raw = (event.raw ?? null);
|
|
7
|
-
const kind = raw?.kind;
|
|
8
|
-
const baseContext = event.context;
|
|
9
|
-
const common = {
|
|
10
|
-
id: event.id,
|
|
11
|
-
parentTraceNodeId: baseContext.causedByNodeId ?? baseContext.originNodeId,
|
|
12
|
-
lifecycle: 'completed',
|
|
13
|
-
context: baseContext,
|
|
14
|
-
startTime: event.timestamp,
|
|
15
|
-
attributes: { ...(raw ?? {}) },
|
|
16
|
-
};
|
|
17
|
-
if (event.source === 'react' && kind === 'render') {
|
|
18
|
-
const r = raw;
|
|
19
|
-
return {
|
|
20
|
-
parentAxis: 'render',
|
|
21
|
-
node: {
|
|
22
|
-
...common,
|
|
23
|
-
type: 'render',
|
|
24
|
-
logicalId: r.render?.componentLogicalId,
|
|
25
|
-
instanceId: r.render?.componentInstanceId,
|
|
26
|
-
name: `render ${r.render?.componentName ?? '?'}`,
|
|
27
|
-
durationMs: r.render?.actualDurationMs,
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
if (event.source === 'valtio' && kind === 'state-update') {
|
|
32
|
-
const r = raw;
|
|
33
|
-
return {
|
|
34
|
-
parentAxis: 'state',
|
|
35
|
-
node: {
|
|
36
|
-
...common,
|
|
37
|
-
type: 'state-update',
|
|
38
|
-
logicalId: r.update?.storeId,
|
|
39
|
-
name: `${r.update?.storeId ?? '?'}.${r.update?.path ?? '?'}`,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (event.source === 'nestjs' && kind === 'server-request') {
|
|
44
|
-
const r = raw;
|
|
45
|
-
return {
|
|
46
|
-
parentAxis: 'backend',
|
|
47
|
-
node: {
|
|
48
|
-
...common,
|
|
49
|
-
type: 'server-request',
|
|
50
|
-
name: `${r.request?.method ?? '?'} ${r.request?.route ?? '?'}`,
|
|
51
|
-
durationMs: r.request?.durationMs,
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
if (kind === 'loop') {
|
|
56
|
-
const r = raw;
|
|
57
|
-
return {
|
|
58
|
-
parentAxis: 'performance',
|
|
59
|
-
node: {
|
|
60
|
-
...common,
|
|
61
|
-
type: 'loop',
|
|
62
|
-
name: 'loop',
|
|
63
|
-
durationMs: r.loop?.durationMs,
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
if (event.source === 'client-runtime' && kind === 'user-action') {
|
|
68
|
-
const r = raw;
|
|
69
|
-
return {
|
|
70
|
-
parentAxis: 'caused',
|
|
71
|
-
node: {
|
|
72
|
-
...common,
|
|
73
|
-
type: 'ui-event',
|
|
74
|
-
logicalId: r.origin?.nodeId,
|
|
75
|
-
name: r.origin?.label ?? event.title,
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Populate `store` from `events`. Edges are created two ways:
|
|
83
|
-
* 1. explicit `causedByNodeId` → an edge on the node's natural axis.
|
|
84
|
-
* 2. same-flow chronological chaining on the 'caused' axis so a flow's
|
|
85
|
-
* nodes form a walkable spine even when explicit causality is absent.
|
|
86
|
-
*/
|
|
87
|
-
export function buildGraphFromEvents(store, events) {
|
|
88
|
-
const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp);
|
|
89
|
-
const mapped = [];
|
|
90
|
-
for (const e of sorted) {
|
|
91
|
-
const m = eventToNode(e);
|
|
92
|
-
if (!m)
|
|
93
|
-
continue;
|
|
94
|
-
store.addNode(m.node);
|
|
95
|
-
mapped.push(m);
|
|
96
|
-
}
|
|
97
|
-
let edgeCount = 0;
|
|
98
|
-
const addEdge = (from, to, axis, type) => {
|
|
99
|
-
const edge = {
|
|
100
|
-
id: `${axis}:${from}->${to}`,
|
|
101
|
-
from,
|
|
102
|
-
to,
|
|
103
|
-
axis,
|
|
104
|
-
type,
|
|
105
|
-
timestamp: store.getNode(to)?.startTime ?? 0,
|
|
106
|
-
};
|
|
107
|
-
store.addEdge(edge);
|
|
108
|
-
edgeCount++;
|
|
109
|
-
};
|
|
110
|
-
// Explicit causedBy edges.
|
|
111
|
-
for (const m of mapped) {
|
|
112
|
-
const caused = m.node.context.causedByNodeId;
|
|
113
|
-
if (caused && store.getNode(caused)) {
|
|
114
|
-
addEdge(caused, m.node.id, m.parentAxis ?? 'caused', 'caused-by');
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
// Same-flow chronological spine on the caused axis.
|
|
118
|
-
const byFlow = new Map();
|
|
119
|
-
for (const m of mapped) {
|
|
120
|
-
const flowId = m.node.context.flowId;
|
|
121
|
-
if (!flowId)
|
|
122
|
-
continue;
|
|
123
|
-
let arr = byFlow.get(flowId);
|
|
124
|
-
if (!arr) {
|
|
125
|
-
arr = [];
|
|
126
|
-
byFlow.set(flowId, arr);
|
|
127
|
-
}
|
|
128
|
-
arr.push(m);
|
|
129
|
-
}
|
|
130
|
-
for (const chain of byFlow.values()) {
|
|
131
|
-
for (let i = 1; i < chain.length; i++) {
|
|
132
|
-
const prev = chain[i - 1].node;
|
|
133
|
-
const cur = chain[i].node;
|
|
134
|
-
// Skip if an explicit edge already connects them.
|
|
135
|
-
if (cur.context.causedByNodeId === prev.id)
|
|
136
|
-
continue;
|
|
137
|
-
addEdge(prev.id, cur.id, 'caused', 'flow-sequence');
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return { nodeCount: mapped.length, edgeCount };
|
|
141
|
-
}
|
|
1
|
+
"use strict";var g=Object.defineProperty;var u=(t,r)=>g(t,"name",{value:r,configurable:!0});var I=Object.defineProperty,l=u((t,r)=>I(t,"name",{value:r,configurable:!0}),"u");function m(t){const r=t.raw??null,c=r?.kind,s=t.context,a={id:t.id,parentTraceNodeId:s.causedByNodeId??s.originNodeId,lifecycle:"completed",context:s,startTime:t.timestamp,attributes:{...r??{}}};if(t.source==="react"&&c==="render"){const n=r;return{parentAxis:"render",node:{...a,type:"render",logicalId:n.render?.componentLogicalId,instanceId:n.render?.componentInstanceId,name:`render ${n.render?.componentName??"?"}`,durationMs:n.render?.actualDurationMs}}}if(t.source==="valtio"&&c==="state-update"){const n=r;return{parentAxis:"state",node:{...a,type:"state-update",logicalId:n.update?.storeId,name:`${n.update?.storeId??"?"}.${n.update?.path??"?"}`}}}if(t.source==="nestjs"&&c==="server-request"){const n=r;return{parentAxis:"backend",node:{...a,type:"server-request",name:`${n.request?.method??"?"} ${n.request?.route??"?"}`,durationMs:n.request?.durationMs}}}if(c==="loop")return{parentAxis:"performance",node:{...a,type:"loop",name:"loop",durationMs:r.loop?.durationMs}};if(t.source==="client-runtime"&&c==="user-action"){const n=r;return{parentAxis:"caused",node:{...a,type:"ui-event",logicalId:n.origin?.nodeId,name:n.origin?.label??t.title}}}}u(m,"f"),l(m,"eventToNode");export function buildGraphFromEvents(t,r){const c=[...r].sort((o,e)=>o.timestamp-e.timestamp),s=[];for(const o of c){const e=m(o);e&&(t.addNode(e.node),s.push(e))}let a=0;const n=l((o,e,d,i)=>{const f={id:`${d}:${o}->${e}`,from:o,to:e,axis:d,type:i,timestamp:t.getNode(e)?.startTime??0};t.addEdge(f),a++},"addEdge");for(const o of s){const e=o.node.context.causedByNodeId;e&&t.getNode(e)&&n(e,o.node.id,o.parentAxis??"caused","caused-by")}const p=new Map;for(const o of s){const e=o.node.context.flowId;if(!e)continue;let d=p.get(e);d||(d=[],p.set(e,d)),d.push(o)}for(const o of p.values())for(let e=1;e<o.length;e++){const d=o[e-1].node,i=o[e].node;i.context.causedByNodeId!==d.id&&n(d.id,i.id,"caused","flow-sequence")}return{nodeCount:s.length,edgeCount:a}}u(buildGraphFromEvents,"buildGraphFromEvents"),l(buildGraphFromEvents,"buildGraphFromEvents");
|
package/lib/graph-store.js
CHANGED
|
@@ -1,171 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const DEFAULT_MAX_NODES = 1000;
|
|
3
|
-
/**
|
|
4
|
-
* Typed multi-axis trace graph. Hot-tier only (in-memory). Nodes and
|
|
5
|
-
* edges are indexed for cheap per-axis traversal.
|
|
6
|
-
*/
|
|
7
|
-
export class GraphStore {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.nodes = new Map();
|
|
10
|
-
this.edges = new Map();
|
|
11
|
-
// Indices
|
|
12
|
-
this.byParent = new Map();
|
|
13
|
-
this.byFlow = new Map();
|
|
14
|
-
this.byLogical = new Map();
|
|
15
|
-
this.byInstance = new Map();
|
|
16
|
-
this.byRequest = new Map();
|
|
17
|
-
/** axis → { from-nodeId → Set<edgeId> } */
|
|
18
|
-
this.outByAxis = new Map();
|
|
19
|
-
/** axis → { to-nodeId → Set<edgeId> } */
|
|
20
|
-
this.inByAxis = new Map();
|
|
21
|
-
}
|
|
22
|
-
addNode(node) {
|
|
23
|
-
this.nodes.set(node.id, node);
|
|
24
|
-
if (node.parentTraceNodeId) {
|
|
25
|
-
this.addToIndex(this.byParent, node.parentTraceNodeId, node.id);
|
|
26
|
-
}
|
|
27
|
-
if (node.context.flowId) {
|
|
28
|
-
this.addToIndex(this.byFlow, node.context.flowId, node.id);
|
|
29
|
-
}
|
|
30
|
-
if (node.logicalId) {
|
|
31
|
-
this.addToIndex(this.byLogical, node.logicalId, node.id);
|
|
32
|
-
}
|
|
33
|
-
if (node.instanceId) {
|
|
34
|
-
this.addToIndex(this.byInstance, node.instanceId, node.id);
|
|
35
|
-
}
|
|
36
|
-
if (node.context.requestId) {
|
|
37
|
-
this.addToIndex(this.byRequest, node.context.requestId, node.id);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
updateNode(id, patch) {
|
|
41
|
-
const existing = this.nodes.get(id);
|
|
42
|
-
if (!existing)
|
|
43
|
-
throw new Error(`Unknown node: ${id}`);
|
|
44
|
-
const next = { ...existing, ...patch, id: existing.id };
|
|
45
|
-
this.nodes.set(id, next);
|
|
46
|
-
return next;
|
|
47
|
-
}
|
|
48
|
-
getNode(id) {
|
|
49
|
-
return this.nodes.get(id);
|
|
50
|
-
}
|
|
51
|
-
addEdge(edge) {
|
|
52
|
-
this.edges.set(edge.id, edge);
|
|
53
|
-
let outMap = this.outByAxis.get(edge.axis);
|
|
54
|
-
if (!outMap) {
|
|
55
|
-
outMap = new Map();
|
|
56
|
-
this.outByAxis.set(edge.axis, outMap);
|
|
57
|
-
}
|
|
58
|
-
this.addToIndex(outMap, edge.from, edge.id);
|
|
59
|
-
let inMap = this.inByAxis.get(edge.axis);
|
|
60
|
-
if (!inMap) {
|
|
61
|
-
inMap = new Map();
|
|
62
|
-
this.inByAxis.set(edge.axis, inMap);
|
|
63
|
-
}
|
|
64
|
-
this.addToIndex(inMap, edge.to, edge.id);
|
|
65
|
-
}
|
|
66
|
-
getEdge(id) {
|
|
67
|
-
return this.edges.get(id);
|
|
68
|
-
}
|
|
69
|
-
// ---------- Indexed lookups ----------
|
|
70
|
-
childrenOf(parentId) {
|
|
71
|
-
return this.materialiseNodes(this.byParent.get(parentId));
|
|
72
|
-
}
|
|
73
|
-
nodesInFlow(flowId) {
|
|
74
|
-
return this.materialiseNodes(this.byFlow.get(flowId));
|
|
75
|
-
}
|
|
76
|
-
nodesByLogical(logicalId) {
|
|
77
|
-
return this.materialiseNodes(this.byLogical.get(logicalId));
|
|
78
|
-
}
|
|
79
|
-
nodesByInstance(instanceId) {
|
|
80
|
-
return this.materialiseNodes(this.byInstance.get(instanceId));
|
|
81
|
-
}
|
|
82
|
-
nodesByRequest(requestId) {
|
|
83
|
-
return this.materialiseNodes(this.byRequest.get(requestId));
|
|
84
|
-
}
|
|
85
|
-
// ---------- Lens traversal ----------
|
|
86
|
-
traverse(opts) {
|
|
87
|
-
const start = this.nodes.get(opts.from);
|
|
88
|
-
if (!start)
|
|
89
|
-
return { nodes: [], edges: [] };
|
|
90
|
-
const axes = Array.isArray(opts.lens)
|
|
91
|
-
? opts.lens
|
|
92
|
-
: [opts.lens];
|
|
93
|
-
const direction = opts.direction ?? 'both';
|
|
94
|
-
const depth = opts.depth ?? DEFAULT_DEPTH;
|
|
95
|
-
const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
|
|
96
|
-
const visitedNodes = new Set();
|
|
97
|
-
const visitedEdges = new Set();
|
|
98
|
-
const queue = [{ id: start.id, d: 0 }];
|
|
99
|
-
while (queue.length) {
|
|
100
|
-
if (visitedNodes.size >= maxNodes)
|
|
101
|
-
break;
|
|
102
|
-
const { id, d } = queue.shift();
|
|
103
|
-
if (visitedNodes.has(id))
|
|
104
|
-
continue;
|
|
105
|
-
visitedNodes.add(id);
|
|
106
|
-
if (d >= depth)
|
|
107
|
-
continue;
|
|
108
|
-
for (const axis of axes) {
|
|
109
|
-
if (direction === 'out' || direction === 'both') {
|
|
110
|
-
const outMap = this.outByAxis.get(axis);
|
|
111
|
-
const edgeIds = outMap?.get(id);
|
|
112
|
-
if (edgeIds) {
|
|
113
|
-
for (const eid of edgeIds) {
|
|
114
|
-
visitedEdges.add(eid);
|
|
115
|
-
const edge = this.edges.get(eid);
|
|
116
|
-
if (!visitedNodes.has(edge.to)) {
|
|
117
|
-
queue.push({ id: edge.to, d: d + 1 });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (direction === 'in' || direction === 'both') {
|
|
123
|
-
const inMap = this.inByAxis.get(axis);
|
|
124
|
-
const edgeIds = inMap?.get(id);
|
|
125
|
-
if (edgeIds) {
|
|
126
|
-
for (const eid of edgeIds) {
|
|
127
|
-
visitedEdges.add(eid);
|
|
128
|
-
const edge = this.edges.get(eid);
|
|
129
|
-
if (!visitedNodes.has(edge.from)) {
|
|
130
|
-
queue.push({ id: edge.from, d: d + 1 });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
nodes: this.materialiseNodes(visitedNodes),
|
|
139
|
-
edges: [...visitedEdges]
|
|
140
|
-
.map((eid) => this.edges.get(eid))
|
|
141
|
-
.filter((e) => Boolean(e)),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
// ---------- Stats ----------
|
|
145
|
-
nodeCount() {
|
|
146
|
-
return this.nodes.size;
|
|
147
|
-
}
|
|
148
|
-
edgeCount() {
|
|
149
|
-
return this.edges.size;
|
|
150
|
-
}
|
|
151
|
-
// ---------- internals ----------
|
|
152
|
-
addToIndex(index, key, value) {
|
|
153
|
-
let set = index.get(key);
|
|
154
|
-
if (!set) {
|
|
155
|
-
set = new Set();
|
|
156
|
-
index.set(key, set);
|
|
157
|
-
}
|
|
158
|
-
set.add(value);
|
|
159
|
-
}
|
|
160
|
-
materialiseNodes(ids) {
|
|
161
|
-
if (!ids)
|
|
162
|
-
return [];
|
|
163
|
-
const out = [];
|
|
164
|
-
for (const id of ids) {
|
|
165
|
-
const n = this.nodes.get(id);
|
|
166
|
-
if (n)
|
|
167
|
-
out.push(n);
|
|
168
|
-
}
|
|
169
|
-
return out;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
1
|
+
"use strict";var w=Object.defineProperty;var l=(h,e)=>w(h,"name",{value:e,configurable:!0});var p=Object.defineProperty,I=l((h,e)=>p(h,"name",{value:e,configurable:!0}),"l");const m=4,N=1e3;export class GraphStore{static{l(this,"GraphStore")}static{I(this,"GraphStore")}constructor(){this.nodes=new Map,this.edges=new Map,this.byParent=new Map,this.byFlow=new Map,this.byLogical=new Map,this.byInstance=new Map,this.byRequest=new Map,this.outByAxis=new Map,this.inByAxis=new Map}addNode(e){this.nodes.set(e.id,e),e.parentTraceNodeId&&this.addToIndex(this.byParent,e.parentTraceNodeId,e.id),e.context.flowId&&this.addToIndex(this.byFlow,e.context.flowId,e.id),e.logicalId&&this.addToIndex(this.byLogical,e.logicalId,e.id),e.instanceId&&this.addToIndex(this.byInstance,e.instanceId,e.id),e.context.requestId&&this.addToIndex(this.byRequest,e.context.requestId,e.id)}updateNode(e,s){const i=this.nodes.get(e);if(!i)throw new Error(`Unknown node: ${e}`);const t={...i,...s,id:i.id};return this.nodes.set(e,t),t}getNode(e){return this.nodes.get(e)}addEdge(e){this.edges.set(e.id,e);let s=this.outByAxis.get(e.axis);s||(s=new Map,this.outByAxis.set(e.axis,s)),this.addToIndex(s,e.from,e.id);let i=this.inByAxis.get(e.axis);i||(i=new Map,this.inByAxis.set(e.axis,i)),this.addToIndex(i,e.to,e.id)}getEdge(e){return this.edges.get(e)}childrenOf(e){return this.materialiseNodes(this.byParent.get(e))}nodesInFlow(e){return this.materialiseNodes(this.byFlow.get(e))}nodesByLogical(e){return this.materialiseNodes(this.byLogical.get(e))}nodesByInstance(e){return this.materialiseNodes(this.byInstance.get(e))}nodesByRequest(e){return this.materialiseNodes(this.byRequest.get(e))}traverse(e){const s=this.nodes.get(e.from);if(!s)return{nodes:[],edges:[]};const i=Array.isArray(e.lens)?e.lens:[e.lens],t=e.direction??"both",f=e.depth??4,x=e.maxNodes??1e3,n=new Set,g=new Set,c=[{id:s.id,d:0}];for(;c.length&&!(n.size>=x);){const{id:d,d:u}=c.shift();if(!n.has(d)&&(n.add(d),!(u>=f)))for(const y of i){if(t==="out"||t==="both"){const o=this.outByAxis.get(y)?.get(d);if(o)for(const a of o){g.add(a);const r=this.edges.get(a);n.has(r.to)||c.push({id:r.to,d:u+1})}}if(t==="in"||t==="both"){const o=this.inByAxis.get(y)?.get(d);if(o)for(const a of o){g.add(a);const r=this.edges.get(a);n.has(r.from)||c.push({id:r.from,d:u+1})}}}}return{nodes:this.materialiseNodes(n),edges:[...g].map(d=>this.edges.get(d)).filter(d=>!!d)}}nodeCount(){return this.nodes.size}edgeCount(){return this.edges.size}addToIndex(e,s,i){let t=e.get(s);t||(t=new Set,e.set(s,t)),t.add(i)}materialiseNodes(e){if(!e)return[];const s=[];for(const i of e){const t=this.nodes.get(i);t&&s.push(t)}return s}}
|
package/lib/patterns.js
CHANGED
|
@@ -1,163 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Flag `loop` events whose `dbCallsInsideLoop` exceeds the threshold.
|
|
3
|
-
* Events are expected to carry `raw.kind === 'loop'` with a `loop`
|
|
4
|
-
* attribute matching `LoopAttrs`.
|
|
5
|
-
*/
|
|
6
|
-
export function detectDbInLoop(events, options = {}) {
|
|
7
|
-
const threshold = options.threshold ?? 5;
|
|
8
|
-
const out = [];
|
|
9
|
-
for (const e of events) {
|
|
10
|
-
const raw = (e.raw ?? null);
|
|
11
|
-
if (raw?.kind !== 'loop' || !raw.loop)
|
|
12
|
-
continue;
|
|
13
|
-
const dbCalls = raw.loop.dbCallsInsideLoop ?? 0;
|
|
14
|
-
if (dbCalls > threshold) {
|
|
15
|
-
out.push({
|
|
16
|
-
kind: 'db-in-loop',
|
|
17
|
-
severity: 'warning',
|
|
18
|
-
title: `${dbCalls} DB calls inside a loop (${raw.loop.iterations ?? '?'} iterations)`,
|
|
19
|
-
evidence: [e.id],
|
|
20
|
-
details: {
|
|
21
|
-
dbCallsInsideLoop: dbCalls,
|
|
22
|
-
iterations: raw.loop.iterations,
|
|
23
|
-
durationMs: raw.loop.durationMs,
|
|
24
|
-
threshold,
|
|
25
|
-
},
|
|
26
|
-
detectedAt: e.timestamp,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Flag repeated identical DB query signatures. Events are expected to
|
|
34
|
-
* carry `raw.kind === 'db-query'` with `raw.query.signature`.
|
|
35
|
-
*/
|
|
36
|
-
export function detectNPlusOne(events, options = {}) {
|
|
37
|
-
const threshold = options.threshold ?? 3;
|
|
38
|
-
const groups = new Map();
|
|
39
|
-
for (const e of events) {
|
|
40
|
-
const raw = (e.raw ?? null);
|
|
41
|
-
if (raw?.kind !== 'db-query' || !raw.query?.signature)
|
|
42
|
-
continue;
|
|
43
|
-
const sig = raw.query.signature;
|
|
44
|
-
let arr = groups.get(sig);
|
|
45
|
-
if (!arr) {
|
|
46
|
-
arr = [];
|
|
47
|
-
groups.set(sig, arr);
|
|
48
|
-
}
|
|
49
|
-
arr.push(e);
|
|
50
|
-
}
|
|
51
|
-
const out = [];
|
|
52
|
-
for (const [signature, group] of groups) {
|
|
53
|
-
if (group.length >= threshold) {
|
|
54
|
-
out.push({
|
|
55
|
-
kind: 'n+1-query',
|
|
56
|
-
severity: 'warning',
|
|
57
|
-
title: `Query repeated ${group.length}× — likely N+1`,
|
|
58
|
-
evidence: group.map((e) => e.id),
|
|
59
|
-
details: { signature, count: group.length, threshold },
|
|
60
|
-
detectedAt: group[group.length - 1].timestamp,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Flag a component instance that rendered more than `threshold` times
|
|
68
|
-
* inside any `windowMs` window. Events are `source:'react'`,
|
|
69
|
-
* `raw.kind === 'render'`, `raw.render.componentInstanceId`.
|
|
70
|
-
*/
|
|
71
|
-
export function detectRenderStorm(events, options = {}) {
|
|
72
|
-
const threshold = options.threshold ?? 5;
|
|
73
|
-
const windowMs = options.windowMs ?? 250;
|
|
74
|
-
const byInstance = new Map();
|
|
75
|
-
for (const e of events) {
|
|
76
|
-
if (e.source !== 'react')
|
|
77
|
-
continue;
|
|
78
|
-
const raw = (e.raw ?? null);
|
|
79
|
-
if (raw?.kind !== 'render' || !raw.render?.componentInstanceId)
|
|
80
|
-
continue;
|
|
81
|
-
const id = raw.render.componentInstanceId;
|
|
82
|
-
let arr = byInstance.get(id);
|
|
83
|
-
if (!arr) {
|
|
84
|
-
arr = [];
|
|
85
|
-
byInstance.set(id, arr);
|
|
86
|
-
}
|
|
87
|
-
arr.push(e);
|
|
88
|
-
}
|
|
89
|
-
const out = [];
|
|
90
|
-
for (const [instanceId, group] of byInstance) {
|
|
91
|
-
group.sort((a, b) => a.timestamp - b.timestamp);
|
|
92
|
-
// Sliding window: find the densest run.
|
|
93
|
-
let start = 0;
|
|
94
|
-
for (let end = 0; end < group.length; end++) {
|
|
95
|
-
while (group[end].timestamp - group[start].timestamp > windowMs) {
|
|
96
|
-
start++;
|
|
97
|
-
}
|
|
98
|
-
const count = end - start + 1;
|
|
99
|
-
if (count > threshold) {
|
|
100
|
-
const window = group.slice(start, end + 1);
|
|
101
|
-
const name = window[0].raw.render
|
|
102
|
-
?.componentName ?? instanceId;
|
|
103
|
-
out.push({
|
|
104
|
-
kind: 'render-storm',
|
|
105
|
-
severity: 'warning',
|
|
106
|
-
title: `${name} rendered ${count}× in ${windowMs}ms`,
|
|
107
|
-
evidence: window.map((e) => e.id),
|
|
108
|
-
details: {
|
|
109
|
-
componentInstanceId: instanceId,
|
|
110
|
-
count,
|
|
111
|
-
windowMs,
|
|
112
|
-
threshold,
|
|
113
|
-
},
|
|
114
|
-
detectedAt: window[window.length - 1].timestamp,
|
|
115
|
-
});
|
|
116
|
-
break; // one match per instance is enough
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return out;
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Find the single slowest cumulative chain on the caused/async axis,
|
|
124
|
-
* given a precomputed adjacency (nodeId → outgoing nodeIds) and a
|
|
125
|
-
* per-node duration map. Pure + bounded (visits each node once via
|
|
126
|
-
* memoised DFS).
|
|
127
|
-
*/
|
|
128
|
-
export function findSlowPath(roots, adjacency, durationOf) {
|
|
129
|
-
const memo = new Map();
|
|
130
|
-
const inProgress = new Set();
|
|
131
|
-
function best(nodeId) {
|
|
132
|
-
const cached = memo.get(nodeId);
|
|
133
|
-
if (cached)
|
|
134
|
-
return cached;
|
|
135
|
-
if (inProgress.has(nodeId)) {
|
|
136
|
-
// cycle guard
|
|
137
|
-
return { path: [nodeId], totalDurationMs: durationOf(nodeId) };
|
|
138
|
-
}
|
|
139
|
-
inProgress.add(nodeId);
|
|
140
|
-
const selfMs = durationOf(nodeId);
|
|
141
|
-
const children = adjacency.get(nodeId) ?? [];
|
|
142
|
-
let bestChild;
|
|
143
|
-
for (const child of children) {
|
|
144
|
-
const r = best(child);
|
|
145
|
-
if (!bestChild || r.totalDurationMs > bestChild.totalDurationMs) {
|
|
146
|
-
bestChild = r;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
inProgress.delete(nodeId);
|
|
150
|
-
const result = bestChild
|
|
151
|
-
? { path: [nodeId, ...bestChild.path], totalDurationMs: selfMs + bestChild.totalDurationMs }
|
|
152
|
-
: { path: [nodeId], totalDurationMs: selfMs };
|
|
153
|
-
memo.set(nodeId, result);
|
|
154
|
-
return result;
|
|
155
|
-
}
|
|
156
|
-
let overall = { path: [], totalDurationMs: 0 };
|
|
157
|
-
for (const root of roots) {
|
|
158
|
-
const r = best(root);
|
|
159
|
-
if (r.totalDurationMs > overall.totalDurationMs)
|
|
160
|
-
overall = r;
|
|
161
|
-
}
|
|
162
|
-
return overall;
|
|
163
|
-
}
|
|
1
|
+
"use strict";var g=Object.defineProperty;var p=(c,a)=>g(c,"name",{value:a,configurable:!0});var M=Object.defineProperty,f=p((c,a)=>M(c,"name",{value:a,configurable:!0}),"p");export function detectDbInLoop(c,a={}){const d=a.threshold??5,i=[];for(const s of c){const n=s.raw??null;if(n?.kind!=="loop"||!n.loop)continue;const e=n.loop.dbCallsInsideLoop??0;e>d&&i.push({kind:"db-in-loop",severity:"warning",title:`${e} DB calls inside a loop (${n.loop.iterations??"?"} iterations)`,evidence:[s.id],details:{dbCallsInsideLoop:e,iterations:n.loop.iterations,durationMs:n.loop.durationMs,threshold:d},detectedAt:s.timestamp})}return i}p(detectDbInLoop,"detectDbInLoop"),f(detectDbInLoop,"detectDbInLoop");export function detectNPlusOne(c,a={}){const d=a.threshold??3,i=new Map;for(const n of c){const e=n.raw??null;if(e?.kind!=="db-query"||!e.query?.signature)continue;const t=e.query.signature;let o=i.get(t);o||(o=[],i.set(t,o)),o.push(n)}const s=[];for(const[n,e]of i)e.length>=d&&s.push({kind:"n+1-query",severity:"warning",title:`Query repeated ${e.length}\xD7 \u2014 likely N+1`,evidence:e.map(t=>t.id),details:{signature:n,count:e.length,threshold:d},detectedAt:e[e.length-1].timestamp});return s}p(detectNPlusOne,"detectNPlusOne"),f(detectNPlusOne,"detectNPlusOne");export function detectRenderStorm(c,a={}){const d=a.threshold??5,i=a.windowMs??250,s=new Map;for(const e of c){if(e.source!=="react")continue;const t=e.raw??null;if(t?.kind!=="render"||!t.render?.componentInstanceId)continue;const o=t.render.componentInstanceId;let r=s.get(o);r||(r=[],s.set(o,r)),r.push(e)}const n=[];for(const[e,t]of s){t.sort((r,u)=>r.timestamp-u.timestamp);let o=0;for(let r=0;r<t.length;r++){for(;t[r].timestamp-t[o].timestamp>i;)o++;const u=r-o+1;if(u>d){const l=t.slice(o,r+1),h=l[0].raw.render?.componentName??e;n.push({kind:"render-storm",severity:"warning",title:`${h} rendered ${u}\xD7 in ${i}ms`,evidence:l.map(m=>m.id),details:{componentInstanceId:e,count:u,windowMs:i,threshold:d},detectedAt:l[l.length-1].timestamp});break}}}return n}p(detectRenderStorm,"detectRenderStorm"),f(detectRenderStorm,"detectRenderStorm");export function findSlowPath(c,a,d){const i=new Map,s=new Set;function n(t){const o=i.get(t);if(o)return o;if(s.has(t))return{path:[t],totalDurationMs:d(t)};s.add(t);const r=d(t),u=a.get(t)??[];let l;for(const m of u){const w=n(m);(!l||w.totalDurationMs>l.totalDurationMs)&&(l=w)}s.delete(t);const h=l?{path:[t,...l.path],totalDurationMs:r+l.totalDurationMs}:{path:[t],totalDurationMs:r};return i.set(t,h),h}p(n,"o"),f(n,"best");let e={path:[],totalDurationMs:0};for(const t of c){const o=n(t);o.totalDurationMs>e.totalDurationMs&&(e=o)}return e}p(findSlowPath,"findSlowPath"),f(findSlowPath,"findSlowPath");
|
package/lib/redact.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction + bounding for anything LensMCP writes to the event bus.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists ─────────────────────────────────────────────────────────
|
|
5
|
+
* Until the browser taps were enriched they recorded almost nothing about an
|
|
6
|
+
* error, so there was nothing to leak. Enriching them (message, stack,
|
|
7
|
+
* method/URL/status) changed that: a request URL routinely carries a
|
|
8
|
+
* credential in its query string (`?access_token=…`, `?code=` on an OAuth
|
|
9
|
+
* callback, `?key=` on a Google API), and an error message can quote a JWT or
|
|
10
|
+
* an `Authorization` header. Those values then land in `runtime://browser/*`,
|
|
11
|
+
* which an agent reads and may quote into a transcript.
|
|
12
|
+
*
|
|
13
|
+
* ── Why it lives in `core` and not in the bridge ────────────────────────────
|
|
14
|
+
* It shipped inside `@lensmcp/bridge`, applied in `envelopeToEvent` — the ONE
|
|
15
|
+
* function every BROWSER envelope crosses. That covered the browser and only
|
|
16
|
+
* the browser. The GATEWAY publishes its own events straight onto the same bus
|
|
17
|
+
* without ever crossing that function, so it stayed unredacted — and the
|
|
18
|
+
* gateway sees every request from every app in the workspace, which makes it
|
|
19
|
+
* the higher-volume path by a wide margin. Observed on a real machine
|
|
20
|
+
* (foodguard, 1.18.4): a single request wrote
|
|
21
|
+
* `?access_token=sk_live_…` verbatim into `title`, `raw.url` AND
|
|
22
|
+
* `raw.steps[].url` of `.lensmcp/events.jsonl`.
|
|
23
|
+
*
|
|
24
|
+
* So the module moved DOWN to `layer:core`, where both the browser bridge
|
|
25
|
+
* (`layer:host`) and the cluster gateway (`layer:install`) can reach it. There
|
|
26
|
+
* is exactly one implementation; a second one would diverge and one of the two
|
|
27
|
+
* would be the one that misses. `@lensmcp/bridge` re-exports every symbol, so
|
|
28
|
+
* its public API is unchanged.
|
|
29
|
+
*
|
|
30
|
+
* It is imported through the dependency-free `@lensmcp/core/redact` subpath
|
|
31
|
+
* rather than the package root, so the prod gateway pulls in this one file and
|
|
32
|
+
* not the whole core graph (event bus, graph store, zod).
|
|
33
|
+
*
|
|
34
|
+
* `nx.json` has carried a `redaction: { headers, paths }` block since the init
|
|
35
|
+
* generator was written, with no reader anywhere in the repo. This is the
|
|
36
|
+
* first code to honour that intent; the lists below are a superset of it.
|
|
37
|
+
*
|
|
38
|
+
* ── Deliberately NOT done here ──────────────────────────────────────────────
|
|
39
|
+
* • Request/response HEADERS are never captured in the first place (the
|
|
40
|
+
* browser taps read none, and the gateway trace records none), which is the
|
|
41
|
+
* strongest possible defence — nothing to redact. Keep it that way; do not
|
|
42
|
+
* add header capture without extending this module.
|
|
43
|
+
* • Request/response BODIES are likewise never captured.
|
|
44
|
+
*/
|
|
45
|
+
/** Replacement for a redacted value. Short, obvious, greppable. */
|
|
46
|
+
export declare const REDACTED = "REDACTED";
|
|
47
|
+
/**
|
|
48
|
+
* Bounds. `raw` is structurally clamped to 32KiB by the bus transform
|
|
49
|
+
* (`createEventClamp`, wired in `@lensmcp/session`), but `title`/`message` are
|
|
50
|
+
* NOT — they are plain strings on `BaseEvent` and reach every reader
|
|
51
|
+
* unclamped. A stack from a deep React tree is routinely tens of KB, so these
|
|
52
|
+
* are the bounds that actually keep an event small.
|
|
53
|
+
*/
|
|
54
|
+
export declare const MAX_TITLE_CHARS = 300;
|
|
55
|
+
export declare const MAX_MESSAGE_CHARS = 4096;
|
|
56
|
+
export declare const MAX_URL_CHARS = 1024;
|
|
57
|
+
/** Is this query-parameter name carrying a credential? */
|
|
58
|
+
export declare function isSecretParam(name: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Strip credentials from a URL — userinfo, then any query OR fragment
|
|
61
|
+
* parameter whose NAME looks like a credential — preserving every other byte
|
|
62
|
+
* and applying no length bound.
|
|
63
|
+
*
|
|
64
|
+
* Split out of {@link redactUrl} because the same pass has to run over strings
|
|
65
|
+
* that are not *only* a URL: a gateway event's title is
|
|
66
|
+
* `gateway GET host/path?a=1&token=… → 401 (3ms)`, and bounding that at
|
|
67
|
+
* `MAX_URL_CHARS` would be the wrong limit. `PARAM_RE` is not anchored, so it
|
|
68
|
+
* finds the pairs wherever they sit in the surrounding prose, and
|
|
69
|
+
* {@link isSecretParam} keeps `?limit=1` and `→ 401 (3ms)` untouched.
|
|
70
|
+
*
|
|
71
|
+
* Implemented as a regex rewrite rather than `new URL(...)` on purpose:
|
|
72
|
+
* `URL` re-encodes as it round-trips (`+` vs `%20`, dropped default ports,
|
|
73
|
+
* sorted-looking output), which would mean the URL an agent reads is not the
|
|
74
|
+
* URL the app requested. It also throws on relative inputs, which is most of
|
|
75
|
+
* what a SPA fetches. This keeps the string byte-identical apart from the
|
|
76
|
+
* redacted values, and never throws.
|
|
77
|
+
*
|
|
78
|
+
* The fragment matters — an OAuth implicit-flow callback puts the access token
|
|
79
|
+
* there (`#access_token=…`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function redactUrlParams(value: string): string;
|
|
82
|
+
/** {@link redactUrlParams}, bounded — the standard treatment for a captured URL. */
|
|
83
|
+
export declare function redactUrl(url: string): string;
|
|
84
|
+
/**
|
|
85
|
+
* Scrub credentials out of free text (an event title, an error message, a
|
|
86
|
+
* stack, a console line). Cheap, linear, and tuned for precision over recall —
|
|
87
|
+
* a false positive destroys the diagnostic value this whole change is adding.
|
|
88
|
+
*
|
|
89
|
+
* Runs {@link redactUrlParams} FIRST, so a URL embedded in prose is cleaned
|
|
90
|
+
* wherever it appears. The bridge originally left free text alone on the
|
|
91
|
+
* grounds that "widening that means running the URL redactor over every
|
|
92
|
+
* message, which is a cost/false-positive trade not worth making blind" — the
|
|
93
|
+
* gateway leak above made it no longer blind. A gateway event's title IS a URL
|
|
94
|
+
* in prose, and it was the field that leaked. The trade is cheap on both
|
|
95
|
+
* counts: one extra linear pass, and the false-positive surface is
|
|
96
|
+
* {@link isSecretParam}, the exact predicate {@link redactUrl} already trusts
|
|
97
|
+
* (`?author=`/`?keywords=` survive, and a string with no `?name=value` pair
|
|
98
|
+
* comes back byte-identical).
|
|
99
|
+
*/
|
|
100
|
+
export declare function redactText(text: string): string;
|
|
101
|
+
/**
|
|
102
|
+
* Bound a string, marking what was removed. Marker text matches
|
|
103
|
+
* `@lensmcp/storage`'s `clampEventRaw` so the two truncations read the same in
|
|
104
|
+
* a transcript.
|
|
105
|
+
*/
|
|
106
|
+
export declare function clip(value: string, max: number): string;
|
|
107
|
+
/** Redact then bound — the standard treatment for any captured free text. */
|
|
108
|
+
export declare function safeText(value: string | undefined, max?: number): string | undefined;
|
|
109
|
+
/** Redact then bound — the standard treatment for a captured title. */
|
|
110
|
+
export declare function safeTitle(value: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* Scrub credentials out of every string leaf of an arbitrary value.
|
|
113
|
+
*
|
|
114
|
+
* Needed for `console` arguments, which are the one captured payload that is
|
|
115
|
+
* genuinely unstructured — `console.log('auth', { token })` is ordinary
|
|
116
|
+
* debugging code, and the whole point of that change is that those payloads
|
|
117
|
+
* now reach a transcript. It is also what covers a structured `raw`: the
|
|
118
|
+
* gateway's request event carries the URL at `raw.url` AND again at
|
|
119
|
+
* `raw.steps[].url`, and walking every string leaf catches BOTH plus whatever
|
|
120
|
+
* URL-bearing field a future event type adds — which naming the fields
|
|
121
|
+
* one-by-one would not. Bounded on depth and array length so a pathological
|
|
122
|
+
* object cannot make redaction the slow part of a page's logging.
|
|
123
|
+
*
|
|
124
|
+
* Values are returned by reference when nothing changed, so the common case
|
|
125
|
+
* (no credential anywhere) allocates nothing.
|
|
126
|
+
*/
|
|
127
|
+
export declare function redactDeep(value: unknown, depth?: number): unknown;
|
package/lib/redact.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var f=Object.defineProperty;var o=(e,r)=>f(e,"name",{value:r,configurable:!0});var l=Object.defineProperty,a=o((e,r)=>l(e,"name",{value:r,configurable:!0}),"o");export const REDACTED="REDACTED",MAX_TITLE_CHARS=300,MAX_MESSAGE_CHARS=4096,MAX_URL_CHARS=1024;const d=["token","secret","password","passwd","apikey","credential","assertion","signature"],x=new Set(["key","auth","authorization","jwt","code","sig","pwd","pass","session","sid","bearer","otp","cookie","set-cookie"]);export function isSecretParam(e){const r=e.toLowerCase();if(d.some(t=>r.includes(t)))return!0;for(const t of r.split(/[-_.]+/))if(t&&x.has(t))return!0;return!1}o(isSecretParam,"isSecretParam"),a(isSecretParam,"isSecretParam");const A=/^([a-z][a-z0-9+.-]*:\/\/)[^/?#@\s]*@/i,$=/([?&#])([^=&#\s]+)=([^&#\s]*)/g;export function redactUrlParams(e){return e&&e.replace(A,`$1${REDACTED}@`).replace($,(r,t,s)=>isSecretParam(s)?`${t}${s}=${REDACTED}`:r)}o(redactUrlParams,"redactUrlParams"),a(redactUrlParams,"redactUrlParams");export function redactUrl(e){return e&&clip(redactUrlParams(e),1024)}o(redactUrl,"redactUrl"),a(redactUrl,"redactUrl");const _=/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g,b=/\b(bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi,y=/\b(authorization|x-api-key|api-key|cookie|set-cookie|proxy-authorization)\b(\s*[:=]\s*)(["']?)([^\s"',;]{4,})\3/gi;export function redactText(e){return redactUrlParams(e).replace(y,(r,t,s,n)=>`${t}${s}${n}${REDACTED}${n}`).replace(_,REDACTED).replace(b,(r,t)=>`${t} ${REDACTED}`)}o(redactText,"redactText"),a(redactText,"redactText");export function clip(e,r){return e.length<=r?e:`${e.slice(0,r)}\u2026[lensmcp truncated: ${e.length-r} more chars]`}o(clip,"clip"),a(clip,"clip");export function safeText(e,r=4096){if(e!==void 0)return clip(redactText(e),r)}o(safeText,"safeText"),a(safeText,"safeText");export function safeTitle(e){return clip(redactText(e),300)}o(safeTitle,"safeTitle"),a(safeTitle,"safeTitle");const T=6,g=64;export function redactDeep(e,r=0){if(typeof e=="string")return redactText(e);if(e===null||typeof e!="object"||r>=T)return e;if(Array.isArray(e)){let n=!1;const i=e.map((c,p)=>{if(p>=g)return c;const u=redactDeep(c,r+1);return u!==c&&(n=!0),u});return n?i:e}let t=!1;const s={};for(const[n,i]of Object.entries(e)){const c=isSecretParam(n)&&i!==void 0&&i!==null?REDACTED:redactDeep(i,r+1);c!==i&&(t=!0),s[n]=c}return t?s:e}o(redactDeep,"redactDeep"),a(redactDeep,"redactDeep");
|
package/lib/reducer-registry.js
CHANGED
|
@@ -1,28 +1 @@
|
|
|
1
|
-
export class ReducerRegistry {
|
|
2
|
-
constructor() {
|
|
3
|
-
this.reducers = [];
|
|
4
|
-
}
|
|
5
|
-
register(name, reducer) {
|
|
6
|
-
if (this.reducers.some((r) => r.name === name)) {
|
|
7
|
-
throw new Error(`Reducer already registered: ${name}`);
|
|
8
|
-
}
|
|
9
|
-
this.reducers.push({ name, reducer });
|
|
10
|
-
}
|
|
11
|
-
list() {
|
|
12
|
-
return this.reducers;
|
|
13
|
-
}
|
|
14
|
-
/** Run every registered reducer against one event. */
|
|
15
|
-
async dispatch(event) {
|
|
16
|
-
for (const { reducer } of this.reducers) {
|
|
17
|
-
try {
|
|
18
|
-
const r = reducer(event);
|
|
19
|
-
if (r && typeof r.then === 'function') {
|
|
20
|
-
await r;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
/* swallow reducer errors */
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
"use strict";var i=Object.defineProperty;var c=(t,e)=>i(t,"name",{value:e,configurable:!0});var u=Object.defineProperty,a=c((t,e)=>u(t,"name",{value:e,configurable:!0}),"c");export class ReducerRegistry{static{c(this,"ReducerRegistry")}static{a(this,"ReducerRegistry")}constructor(){this.reducers=[]}register(e,s){if(this.reducers.some(r=>r.name===e))throw new Error(`Reducer already registered: ${e}`);this.reducers.push({name:e,reducer:s})}list(){return this.reducers}async dispatch(e){for(const{reducer:s}of this.reducers)try{const r=s(e);r&&typeof r.then=="function"&&await r}catch{}}}
|
package/lib/resource-store.js
CHANGED
|
@@ -1,71 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* The resource store. URIs are registered once; reading is cheap; the
|
|
3
|
-
* `markDirty` call bumps the revision counter and notifies subscribers
|
|
4
|
-
* only when the produced JSON actually changes (diff-aware).
|
|
5
|
-
*/
|
|
6
|
-
export class ResourceStore {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.entries = new Map();
|
|
9
|
-
this.subs = new Map();
|
|
10
|
-
}
|
|
11
|
-
register(uri, produce) {
|
|
12
|
-
if (this.entries.has(uri)) {
|
|
13
|
-
throw new Error(`Resource already registered: ${uri}`);
|
|
14
|
-
}
|
|
15
|
-
this.entries.set(uri, { uri, produce, revision: 0 });
|
|
16
|
-
return () => {
|
|
17
|
-
this.entries.delete(uri);
|
|
18
|
-
this.subs.delete(uri);
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
async read(uri) {
|
|
22
|
-
const entry = this.entries.get(uri);
|
|
23
|
-
if (!entry)
|
|
24
|
-
throw new Error(`Unknown resource: ${uri}`);
|
|
25
|
-
return entry.produce();
|
|
26
|
-
}
|
|
27
|
-
list() {
|
|
28
|
-
return [...this.entries.keys()];
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Recompute the JSON, bump the revision counter if it differs, and
|
|
32
|
-
* notify subscribers. No-op if the URI is unknown or the JSON didn't
|
|
33
|
-
* change.
|
|
34
|
-
*/
|
|
35
|
-
async markDirty(uri) {
|
|
36
|
-
const entry = this.entries.get(uri);
|
|
37
|
-
if (!entry)
|
|
38
|
-
return;
|
|
39
|
-
const next = await entry.produce();
|
|
40
|
-
const serialised = JSON.stringify(next);
|
|
41
|
-
if (serialised === entry.lastJson)
|
|
42
|
-
return;
|
|
43
|
-
entry.lastJson = serialised;
|
|
44
|
-
entry.revision += 1;
|
|
45
|
-
const handlers = this.subs.get(uri);
|
|
46
|
-
if (handlers) {
|
|
47
|
-
for (const h of handlers) {
|
|
48
|
-
try {
|
|
49
|
-
h(uri, entry.revision);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
/* swallow handler errors */
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
revision(uri) {
|
|
58
|
-
return this.entries.get(uri)?.revision;
|
|
59
|
-
}
|
|
60
|
-
subscribe(uri, handler) {
|
|
61
|
-
let set = this.subs.get(uri);
|
|
62
|
-
if (!set) {
|
|
63
|
-
set = new Set();
|
|
64
|
-
this.subs.set(uri, set);
|
|
65
|
-
}
|
|
66
|
-
set.add(handler);
|
|
67
|
-
return () => {
|
|
68
|
-
set?.delete(handler);
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
1
|
+
"use strict";var u=Object.defineProperty;var i=(r,e)=>u(r,"name",{value:e,configurable:!0});var a=Object.defineProperty,h=i((r,e)=>a(r,"name",{value:e,configurable:!0}),"o");export class ResourceStore{static{i(this,"ResourceStore")}static{h(this,"ResourceStore")}constructor(){this.entries=new Map,this.subs=new Map}register(e,t){if(this.entries.has(e))throw new Error(`Resource already registered: ${e}`);return this.entries.set(e,{uri:e,produce:t,revision:0}),()=>{this.entries.delete(e),this.subs.delete(e)}}async read(e){const t=this.entries.get(e);if(!t)throw new Error(`Unknown resource: ${e}`);return t.produce()}list(){return[...this.entries.keys()]}async markDirty(e){const t=this.entries.get(e);if(!t)return;const s=await t.produce(),n=JSON.stringify(s);if(n===t.lastJson)return;t.lastJson=n,t.revision+=1;const o=this.subs.get(e);if(o)for(const c of o)try{c(e,t.revision)}catch{}}revision(e){return this.entries.get(e)?.revision}subscribe(e,t){let s=this.subs.get(e);return s||(s=new Set,this.subs.set(e,s)),s.add(t),()=>{s?.delete(t)}}}
|
package/lib/story.js
CHANGED
|
@@ -1,199 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export function groupByFlow(events) {
|
|
3
|
-
const out = new Map();
|
|
4
|
-
for (const e of events) {
|
|
5
|
-
const flowId = e.context.flowId;
|
|
6
|
-
if (!flowId)
|
|
7
|
-
continue;
|
|
8
|
-
let arr = out.get(flowId);
|
|
9
|
-
if (!arr) {
|
|
10
|
-
arr = [];
|
|
11
|
-
out.set(flowId, arr);
|
|
12
|
-
}
|
|
13
|
-
arr.push(e);
|
|
14
|
-
}
|
|
15
|
-
return out;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Compile a single flow's events into a `FlowStory`. The caller can
|
|
19
|
-
* either pre-filter events (preferred) or pass everything and supply
|
|
20
|
-
* `options.flowId`.
|
|
21
|
-
*/
|
|
22
|
-
export function compileStory(events, options = {}) {
|
|
23
|
-
let pool;
|
|
24
|
-
let flowId;
|
|
25
|
-
if (options.flowId) {
|
|
26
|
-
flowId = options.flowId;
|
|
27
|
-
pool = events.filter((e) => e.context.flowId === flowId);
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
const first = events.find((e) => e.context.flowId);
|
|
31
|
-
if (!first)
|
|
32
|
-
return undefined;
|
|
33
|
-
flowId = first.context.flowId;
|
|
34
|
-
pool = events.filter((e) => e.context.flowId === flowId);
|
|
35
|
-
}
|
|
36
|
-
if (pool.length === 0)
|
|
37
|
-
return undefined;
|
|
38
|
-
pool.sort((a, b) => a.timestamp - b.timestamp);
|
|
39
|
-
// Bound the compiled narrative: a story is a READABLE mechanism trace, and stories are pinned
|
|
40
|
-
// (50 of them) in a long-lived registry — an unbounded flow (event flood, long-lived flowId)
|
|
41
|
-
// must not compile into hundreds of thousands of phases each holding refs into event.raw.
|
|
42
|
-
// Keep the origin plus the most RECENT window (the resolution is what the reader needs).
|
|
43
|
-
const MAX_STORY_EVENTS = 1000;
|
|
44
|
-
if (pool.length > MAX_STORY_EVENTS) {
|
|
45
|
-
pool = [pool[0], ...pool.slice(pool.length - (MAX_STORY_EVENTS - 1))];
|
|
46
|
-
}
|
|
47
|
-
const origin = pool[0];
|
|
48
|
-
const phases = [];
|
|
49
|
-
let finalRender;
|
|
50
|
-
let finalVisualFrame;
|
|
51
|
-
for (const event of pool) {
|
|
52
|
-
const phase = classifyEvent(event);
|
|
53
|
-
if (!phase)
|
|
54
|
-
continue;
|
|
55
|
-
phases.push(phase);
|
|
56
|
-
if (phase.kind === 'render' || phase.kind === 'final-render') {
|
|
57
|
-
const raw = event.raw;
|
|
58
|
-
if (raw?.render) {
|
|
59
|
-
finalRender = {
|
|
60
|
-
id: raw.render.id,
|
|
61
|
-
page: raw.render.page,
|
|
62
|
-
route: raw.render.route,
|
|
63
|
-
componentName: raw.render.componentName,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
else if (event.source === 'visual' && phase.kind === 'visual-violation') {
|
|
68
|
-
const raw = event.raw;
|
|
69
|
-
if (raw?.frame)
|
|
70
|
-
finalVisualFrame = raw.frame.id;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// Promote the final `render` phase (if any) to `final-render`.
|
|
74
|
-
for (let i = phases.length - 1; i >= 0; i--) {
|
|
75
|
-
if (phases[i].kind === 'render') {
|
|
76
|
-
phases[i] = { ...phases[i], kind: 'final-render' };
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
const startedAt = pool[0].timestamp;
|
|
81
|
-
const endedAt = pool[pool.length - 1].timestamp;
|
|
82
|
-
return {
|
|
83
|
-
flowId,
|
|
84
|
-
origin: {
|
|
85
|
-
type: origin.context.originType,
|
|
86
|
-
nodeId: origin.context.originNodeId,
|
|
87
|
-
at: origin.timestamp,
|
|
88
|
-
},
|
|
89
|
-
phases,
|
|
90
|
-
finalState: finalRender
|
|
91
|
-
? {
|
|
92
|
-
page: finalRender.page,
|
|
93
|
-
route: finalRender.route,
|
|
94
|
-
componentName: finalRender.componentName,
|
|
95
|
-
renderId: finalRender.id,
|
|
96
|
-
visualFrameId: finalVisualFrame,
|
|
97
|
-
}
|
|
98
|
-
: undefined,
|
|
99
|
-
startedAt,
|
|
100
|
-
endedAt,
|
|
101
|
-
eventCount: pool.length,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Classify a single event into a story phase. Returns undefined for
|
|
106
|
-
* events that don't carry phase semantics (we still keep them under
|
|
107
|
-
* `flow://flow/{flowId}.events` but don't surface them in the story).
|
|
108
|
-
*/
|
|
109
|
-
function classifyEvent(event) {
|
|
110
|
-
const raw = (event.raw ?? null);
|
|
111
|
-
const kind = raw?.kind;
|
|
112
|
-
// Flow origin events (synthetic user-action emitter)
|
|
113
|
-
if (kind === 'flow-origin' || (event.source === 'client-runtime' && kind === 'user-action')) {
|
|
114
|
-
const r = event.raw;
|
|
115
|
-
return {
|
|
116
|
-
kind: 'user-action',
|
|
117
|
-
at: event.timestamp,
|
|
118
|
-
label: r?.origin?.label ?? event.title,
|
|
119
|
-
eventIds: [event.id],
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
// React renders
|
|
123
|
-
if (event.source === 'react' && kind === 'render') {
|
|
124
|
-
const r = event.raw;
|
|
125
|
-
return {
|
|
126
|
-
kind: 'render',
|
|
127
|
-
at: event.timestamp,
|
|
128
|
-
label: `render ${r?.render?.componentName ?? '?'}`,
|
|
129
|
-
eventIds: [event.id],
|
|
130
|
-
details: r?.render
|
|
131
|
-
? { componentName: r.render.componentName, durationMs: r.render.actualDurationMs }
|
|
132
|
-
: undefined,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
// Slot transitions (loading / idle UI)
|
|
136
|
-
if (event.source === 'react' && kind === 'slot-content-changed') {
|
|
137
|
-
const r = event.raw;
|
|
138
|
-
const to = r?.slot?.toComponent ?? '?';
|
|
139
|
-
const isLoading = /spinner|loader|loading/i.test(to);
|
|
140
|
-
return {
|
|
141
|
-
kind: isLoading ? 'loading-ui' : 'idle-ui',
|
|
142
|
-
at: event.timestamp,
|
|
143
|
-
label: `slot ${r?.slot?.slotLogicalId ?? ''} → ${to}`,
|
|
144
|
-
eventIds: [event.id],
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
// Route transitions (client-runtime emits)
|
|
148
|
-
if (event.source === 'client-runtime' && kind === 'route') {
|
|
149
|
-
const r = event.raw;
|
|
150
|
-
return {
|
|
151
|
-
kind: 'route-transition',
|
|
152
|
-
at: event.timestamp,
|
|
153
|
-
label: `route ${r?.from ?? ''} → ${r?.url ?? ''}`,
|
|
154
|
-
eventIds: [event.id],
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
// Valtio state-update
|
|
158
|
-
if (event.source === 'valtio' && kind === 'state-update') {
|
|
159
|
-
const r = event.raw;
|
|
160
|
-
return {
|
|
161
|
-
kind: 'state-update',
|
|
162
|
-
at: event.timestamp,
|
|
163
|
-
label: `${r?.update?.storeId ?? '?'}.${r?.update?.path ?? '?'}`,
|
|
164
|
-
eventIds: [event.id],
|
|
165
|
-
details: r?.update,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
// Backend (NestJS) server-request
|
|
169
|
-
if (event.source === 'nestjs' && kind === 'server-request') {
|
|
170
|
-
const r = event.raw;
|
|
171
|
-
return {
|
|
172
|
-
kind: 'backend-request',
|
|
173
|
-
at: event.timestamp,
|
|
174
|
-
label: `${r?.request?.method ?? '?'} ${r?.request?.route ?? '?'} → ${r?.request?.status ?? '?'}`,
|
|
175
|
-
eventIds: [event.id],
|
|
176
|
-
details: r?.request,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
// Chrome fetch (browser side) — synthesised by client-runtime.
|
|
180
|
-
if (event.source === 'chrome' && event.category === 'network') {
|
|
181
|
-
return {
|
|
182
|
-
kind: 'backend-request',
|
|
183
|
-
at: event.timestamp,
|
|
184
|
-
label: event.title,
|
|
185
|
-
eventIds: [event.id],
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
// Visual violations
|
|
189
|
-
if (event.source === 'visual' && kind === 'visual-violation') {
|
|
190
|
-
const r = event.raw;
|
|
191
|
-
return {
|
|
192
|
-
kind: 'visual-violation',
|
|
193
|
-
at: event.timestamp,
|
|
194
|
-
label: `${r?.violation?.ruleType ?? 'rule'} ${r?.violation?.ruleId ?? ''}`,
|
|
195
|
-
eventIds: [event.id],
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
1
|
+
"use strict";var w=Object.defineProperty;var u=(e,n)=>w(e,"name",{value:n,configurable:!0});var I=Object.defineProperty,m=u((e,n)=>I(e,"name",{value:n,configurable:!0}),"c");export function groupByFlow(e){const n=new Map;for(const t of e){const o=t.context.flowId;if(!o)continue;let s=n.get(o);s||(s=[],n.set(o,s)),s.push(t)}return n}u(groupByFlow,"groupByFlow"),m(groupByFlow,"groupByFlow");export function compileStory(e,n={}){let t,o;if(n.flowId)o=n.flowId,t=e.filter(r=>r.context.flowId===o);else{const r=e.find(i=>i.context.flowId);if(!r)return;o=r.context.flowId,t=e.filter(i=>i.context.flowId===o)}if(t.length===0)return;t.sort((r,i)=>r.timestamp-i.timestamp);const s=1e3;t.length>s&&(t=[t[0],...t.slice(t.length-(s-1))]);const c=t[0],d=[];let l,p;for(const r of t){const i=f(r);if(i){if(d.push(i),i.kind==="render"||i.kind==="final-render"){const a=r.raw;a?.render&&(l={id:a.render.id,page:a.render.page,route:a.render.route,componentName:a.render.componentName})}else if(r.source==="visual"&&i.kind==="visual-violation"){const a=r.raw;a?.frame&&(p=a.frame.id)}}}for(let r=d.length-1;r>=0;r--)if(d[r].kind==="render"){d[r]={...d[r],kind:"final-render"};break}const v=t[0].timestamp,g=t[t.length-1].timestamp;return{flowId:o,origin:{type:c.context.originType,nodeId:c.context.originNodeId,at:c.timestamp},phases:d,finalState:l?{page:l.page,route:l.route,componentName:l.componentName,renderId:l.id,visualFrameId:p}:void 0,startedAt:v,endedAt:g,eventCount:t.length}}u(compileStory,"compileStory"),m(compileStory,"compileStory");function f(e){const n=(e.raw??null)?.kind;if(n==="flow-origin"||e.source==="client-runtime"&&n==="user-action"){const t=e.raw;return{kind:"user-action",at:e.timestamp,label:t?.origin?.label??e.title,eventIds:[e.id]}}if(e.source==="react"&&n==="render"){const t=e.raw;return{kind:"render",at:e.timestamp,label:`render ${t?.render?.componentName??"?"}`,eventIds:[e.id],details:t?.render?{componentName:t.render.componentName,durationMs:t.render.actualDurationMs}:void 0}}if(e.source==="react"&&n==="slot-content-changed"){const t=e.raw,o=t?.slot?.toComponent??"?";return{kind:/spinner|loader|loading/i.test(o)?"loading-ui":"idle-ui",at:e.timestamp,label:`slot ${t?.slot?.slotLogicalId??""} \u2192 ${o}`,eventIds:[e.id]}}if(e.source==="client-runtime"&&n==="route"){const t=e.raw;return{kind:"route-transition",at:e.timestamp,label:`route ${t?.from??""} \u2192 ${t?.url??""}`,eventIds:[e.id]}}if(e.source==="valtio"&&n==="state-update"){const t=e.raw;return{kind:"state-update",at:e.timestamp,label:`${t?.update?.storeId??"?"}.${t?.update?.path??"?"}`,eventIds:[e.id],details:t?.update}}if(e.source==="nestjs"&&n==="server-request"){const t=e.raw;return{kind:"backend-request",at:e.timestamp,label:`${t?.request?.method??"?"} ${t?.request?.route??"?"} \u2192 ${t?.request?.status??"?"}`,eventIds:[e.id],details:t?.request}}if(e.source==="chrome"&&e.category==="network")return{kind:"backend-request",at:e.timestamp,label:e.title,eventIds:[e.id]};if(e.source==="visual"&&n==="visual-violation"){const t=e.raw;return{kind:"visual-violation",at:e.timestamp,label:`${t?.violation?.ruleType??"rule"} ${t?.violation?.ruleId??""}`,eventIds:[e.id]}}}u(f,"g"),m(f,"classifyEvent");
|
package/lib/thresholds.js
CHANGED
|
@@ -1,23 +1 @@
|
|
|
1
|
-
export const DEFAULT_THRESHOLDS = {
|
|
2
|
-
dbInLoop: 5,
|
|
3
|
-
nPlusOne: 3,
|
|
4
|
-
renderStorm: 5,
|
|
5
|
-
slowRouteMs: 500,
|
|
6
|
-
slowRenderMs: 16,
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Merge partial overrides over the defaults, ignoring any non-finite /
|
|
10
|
-
* non-numeric values (so a malformed config can't disable detection).
|
|
11
|
-
*/
|
|
12
|
-
export function resolveThresholds(overrides) {
|
|
13
|
-
const out = { ...DEFAULT_THRESHOLDS };
|
|
14
|
-
if (overrides) {
|
|
15
|
-
for (const key of Object.keys(DEFAULT_THRESHOLDS)) {
|
|
16
|
-
const v = overrides[key];
|
|
17
|
-
if (typeof v === 'number' && Number.isFinite(v) && v >= 0) {
|
|
18
|
-
out[key] = v;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return out;
|
|
23
|
-
}
|
|
1
|
+
"use strict";var t=Object.defineProperty;var s=(e,r)=>t(e,"name",{value:r,configurable:!0});var c=Object.defineProperty,l=s((e,r)=>c(e,"name",{value:r,configurable:!0}),"s");export const DEFAULT_THRESHOLDS={dbInLoop:5,nPlusOne:3,renderStorm:5,slowRouteMs:500,slowRenderMs:16};export function resolveThresholds(e){const r={...DEFAULT_THRESHOLDS};if(e)for(const n of Object.keys(DEFAULT_THRESHOLDS)){const o=e[n];typeof o=="number"&&Number.isFinite(o)&&o>=0&&(r[n]=o)}return r}s(resolveThresholds,"resolveThresholds"),l(resolveThresholds,"resolveThresholds");
|
package/lib/ulid.js
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Tiny dependency-free ULID-like ID generator.
|
|
3
|
-
*
|
|
4
|
-
* Real ULIDs are 26 chars Crockford-Base32. Ours are 26 chars of the same
|
|
5
|
-
* alphabet for compatibility, with the 48-bit timestamp prefix and 80
|
|
6
|
-
* bits of `crypto.getRandomValues` randomness. Sortable by creation time.
|
|
7
|
-
*/
|
|
8
|
-
const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
9
|
-
const RANDOM_LEN = 16;
|
|
10
|
-
function encodeTime(now, len) {
|
|
11
|
-
let out = '';
|
|
12
|
-
let n = now;
|
|
13
|
-
for (let i = len - 1; i >= 0; i--) {
|
|
14
|
-
const mod = n % 32;
|
|
15
|
-
out = ALPHABET[mod] + out;
|
|
16
|
-
n = (n - mod) / 32;
|
|
17
|
-
}
|
|
18
|
-
return out;
|
|
19
|
-
}
|
|
20
|
-
function encodeRandom(len) {
|
|
21
|
-
const bytes = new Uint8Array(len);
|
|
22
|
-
// Node 24 has global crypto; this is also fine in modern browsers.
|
|
23
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
24
|
-
let out = '';
|
|
25
|
-
for (let i = 0; i < len; i++) {
|
|
26
|
-
out += ALPHABET[(bytes[i] ?? 0) & 31];
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
export function ulid(now = Date.now()) {
|
|
31
|
-
return encodeTime(now, 10) + encodeRandom(RANDOM_LEN);
|
|
32
|
-
}
|
|
1
|
+
"use strict";var d=Object.defineProperty;var r=(e,n)=>d(e,"name",{value:n,configurable:!0});var f=Object.defineProperty,c=r((e,n)=>f(e,"name",{value:n,configurable:!0}),"r");const i="0123456789ABCDEFGHJKMNPQRSTVWXYZ",b=16;function l(e,n){let o="",t=e;for(let u=n-1;u>=0;u--){const a=t%32;o=i[a]+o,t=(t-a)/32}return o}r(l,"s"),c(l,"encodeTime");function s(e){const n=new Uint8Array(e);globalThis.crypto.getRandomValues(n);let o="";for(let t=0;t<e;t++)o+=i[(n[t]??0)&31];return o}r(s,"a"),c(s,"encodeRandom");export function ulid(e=Date.now()){return l(e,10)+s(16)}r(ulid,"ulid"),c(ulid,"ulid");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/core",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
"types": "./index.d.ts",
|
|
12
12
|
"import": "./index.js",
|
|
13
13
|
"default": "./index.js"
|
|
14
|
+
},
|
|
15
|
+
"./redact": {
|
|
16
|
+
"types": "./lib/redact.d.ts",
|
|
17
|
+
"import": "./lib/redact.js",
|
|
18
|
+
"default": "./lib/redact.js"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"dependencies": {
|
|
17
|
-
"@lensmcp/protocol-types": "1.18.
|
|
22
|
+
"@lensmcp/protocol-types": "1.18.7",
|
|
18
23
|
"tslib": "^2.3.0"
|
|
19
24
|
},
|
|
20
25
|
"license": "Apache-2.0",
|