@lensmcp/protocol-types 1.22.5 → 1.23.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/index.d.ts +1 -0
- package/index.js +1 -1
- package/lib/bounded-clone.js +1 -1
- package/lib/bounded-map.js +1 -1
- package/lib/bounded-writer.js +1 -1
- package/lib/dashboard.d.ts +505 -0
- package/lib/dashboard.js +1 -0
- package/lib/flood-guard.js +1 -1
- package/lib/producer-health.js +1 -1
- package/package.json +1 -1
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";export*from"./lib/schema-version.js";export*from"./lib/source-location.js";export*from"./lib/context.js";export*from"./lib/event.js";export*from"./lib/node.js";export*from"./lib/edge.js";export*from"./lib/attributes.js";export*from"./lib/producer-health.js";export*from"./lib/resources.js";export*from"./lib/tokens.js";export*from"./lib/capture-demand.js";export*from"./lib/bounded-clone.js";export*from"./lib/bounded-map.js";export*from"./lib/flood-guard.js";
|
|
1
|
+
"use strict";export*from"./lib/schema-version.js";export*from"./lib/source-location.js";export*from"./lib/context.js";export*from"./lib/event.js";export*from"./lib/node.js";export*from"./lib/edge.js";export*from"./lib/attributes.js";export*from"./lib/producer-health.js";export*from"./lib/resources.js";export*from"./lib/tokens.js";export*from"./lib/capture-demand.js";export*from"./lib/bounded-clone.js";export*from"./lib/bounded-map.js";export*from"./lib/flood-guard.js";export*from"./lib/dashboard.js";
|
package/lib/bounded-clone.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var T=Object.defineProperty;var
|
|
1
|
+
"use strict";var T=Object.defineProperty;var c=(t,e)=>T(t,"name",{value:e,configurable:!0});var h=Object.defineProperty,l=c((t,e)=>h(t,"name",{value:e,configurable:!0}),"i"),_=Object.defineProperty,y=l((t,e)=>_(t,"name",{value:e,configurable:!0}),"_");export const DEFAULT_RAW_BYTE_LIMIT=32768,DEFAULT_MAX_STRING_BYTES=4096,DEFAULT_MAX_ARRAY_ITEMS=64,DEFAULT_MAX_DEPTH=12,CHEAP_ENTRY_BYTES=512,NOMINAL_DEEP_BYTES=32,DEEP_REPORT_DEPTH=64,SKELETON_STRING_BYTES=256,MAX_RECORDED_PATHS=20,TRUNCATION_MARKER_KEY="__lensmcpTruncation",TRUNCATED_VALUE_KEY="__lensmcpTruncated";export function estimateJsonBytes(t,e=Number.POSITIVE_INFINITY,n=12){return g(t,e,n,32)}c(estimateJsonBytes,"estimateJsonBytes"),l(estimateJsonBytes,"estimateJsonBytes"),y(estimateJsonBytes,"estimateJsonBytes");export function exceedsJsonByteBudget(t,e,n=12){return g(t,e,n,Number.POSITIVE_INFINITY)>e}c(exceedsJsonByteBudget,"exceedsJsonByteBudget"),l(exceedsJsonByteBudget,"exceedsJsonByteBudget"),y(exceedsJsonByteBudget,"exceedsJsonByteBudget");function g(t,e,n,a){switch(typeof t){case"undefined":case"function":case"symbol":return 0;case"boolean":return 5;case"number":return Number.isFinite(t)?8:4;case"bigint":return 20;case"string":return t.length+2;default:break}if(t===null)return 4;if(n<=0)return a;if(t instanceof Date)return 26;if(Array.isArray(t)){let r=2;for(let u=0;u<t.length;u++)if(r+=g(t[u],e-r,n-1,a)+1,r>e)return r;return r}let s=2;const o=t;for(const r in o){if(!Object.prototype.hasOwnProperty.call(o,r))continue;const u=g(o[r],e-s,n-1,a);if(u!==0&&(s+=r.length+4+u,s>e))return s}return s}c(g,"y"),l(g,"T"),y(g,"walk");function f(t,e){t.truncatedAnything=!0,t.paths.length<20&&t.paths.push(e||"<root>")}c(f,"g"),l(f,"A"),y(f,"record");function p(t,e,n){return f(t,n),{[TRUNCATED_VALUE_KEY]:!0,originalBytes:estimateJsonBytes(e,Number.POSITIVE_INFINITY,64)}}c(p,"f"),l(p,"I"),y(p,"stub");function m(t){if(t===null)return!0;const e=typeof t;return e==="number"||e==="boolean"||e==="string"||e==="bigint"}c(m,"p"),l(m,"y"),y(m,"isScalar");export function clampValue(t,e,n,a){if(typeof t=="string"){const s=e.budget<=0?256:e.maxStringBytes;if(t.length+2<=s)return e.budget-=t.length+2,t;const o=Math.max(0,s-64);return f(e,a),e.budget-=o,`${t.slice(0,o)}\u2026[lensmcp truncated: ${t.length-o} more chars]`}return t===null||typeof t!="object"?(e.budget-=g(t,Number.POSITIVE_INFINITY,1,32),t):n<=0?p(e,t,a):t instanceof Date?(e.budget-=26,t):Array.isArray(t)?b(t,e,n,a):d(t,e,n,a)}c(clampValue,"clampValue"),l(clampValue,"clampValue"),y(clampValue,"clampValue");function b(t,e,n,a){const s=[],o=Math.min(t.length,e.maxArrayItems);for(let r=0;r<o&&!(e.budget<=0);r++)s.push(clampValue(t[r],e,n-1,`${a}[${r}]`));return s.length<t.length&&f(e,`${a}[] (+${t.length-s.length} items dropped)`),s}c(b,"m"),l(b,"f"),y(b,"clampArray");function d(t,e,n,a){const s={};let o;const r=e.budget<=0;for(const u in t){if(!Object.prototype.hasOwnProperty.call(t,u))continue;const i=t[u],I=g(i,512,n-1,Number.POSITIVE_INFINITY);if(I<=512&&(!r||m(i))){s[u]=i,e.budget-=u.length+4+I;continue}(o??=[]).push({key:u,bytes:I})}if(!o)return s;o.sort((u,i)=>u.bytes===i.bytes?0:u.bytes<i.bytes?-1:1);for(const{key:u}of o){const i=a?`${a}.${u}`:u;s[u]=clampValue(t[u],e,n-1,i)}return s}c(d,"b"),l(d,"m"),y(d,"clampObject");export function boundedClone(t,e={}){const n=e.rawByteLimit??32768,a=e.maxDepth??12;if(n<=0)return{value:void 0,truncated:!1,originalBytes:0,keptBytes:0,paths:[]};if(t==null)return{value:t,truncated:!1,originalBytes:0,keptBytes:0,paths:[]};if(!exceedsJsonByteBudget(t,n,a)){const i=estimateJsonBytes(t,n,a),I={budget:n,maxStringBytes:e.maxStringBytes??4096,maxArrayItems:e.maxArrayItems??64,paths:[],truncatedAnything:!1};return{value:clampValue(t,I,a,""),truncated:!1,originalBytes:i,keptBytes:i,paths:[]}}const s=estimateJsonBytes(t,Number.POSITIVE_INFINITY,64),o={budget:n,maxStringBytes:e.maxStringBytes??4096,maxArrayItems:e.maxArrayItems??64,paths:[],truncatedAnything:!1},r=clampValue(t,o,a,""),u=estimateJsonBytes(r,Number.POSITIVE_INFINITY,a);if(o.truncatedAnything&&r!==null&&typeof r=="object"&&!Array.isArray(r)){const i={truncated:!0,originalBytes:s,keptBytes:u,paths:o.paths};r[TRUNCATION_MARKER_KEY]=i}return{value:r,truncated:o.truncatedAnything,originalBytes:s,keptBytes:u,paths:o.paths}}c(boundedClone,"boundedClone"),l(boundedClone,"boundedClone"),y(boundedClone,"boundedClone");
|
package/lib/bounded-map.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var i=Object.defineProperty;var t=(e,a)=>i(e,"name",{value:a,configurable:!0});var p=Object.defineProperty,o=t((e,a)=>p(e,"name",{value:a,configurable:!0}),"t"),s=Object.defineProperty,u=o((e,a)=>s(e,"name",{value:a,configurable:!0}),"o");export function capMap(e,a){if(e.size<=a)return 0;let r=0;const c=e.keys();for(;e.size>a;){const n=c.next();if(n.done)break;e.delete(n.value)&&(r+=1)}return r}t(capMap,"capMap"),o(capMap,"capMap"),u(capMap,"capMap");export const MAX_PRODUCER_KEYS=256;
|
package/lib/bounded-writer.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var m=Object.defineProperty;var r=(t,e)=>m(t,"name",{value:e,configurable:!0});var f=Object.defineProperty,o=r((t,e)=>f(t,"name",{value:e,configurable:!0}),"r"),x=Object.defineProperty,s=o((t,e)=>x(t,"name",{value:e,configurable:!0}),"c");import{renameSync as v,statSync as M,unlinkSync as E}from"node:fs";export function eventMaxBytes(t=process.env){const e=Number(t.LENSMCP_EVENT_MAX_BYTES);return Number.isFinite(e)&&e>0?e:50*1024*1024}r(eventMaxBytes,"eventMaxBytes"),o(eventMaxBytes,"eventMaxBytes"),s(eventMaxBytes,"eventMaxBytes");export function eventMaxFiles(t=process.env){const e=Number(t.LENSMCP_EVENT_MAX_FILES);return Number.isFinite(e)&&e>=1&&e<=20?Math.floor(e):5}r(eventMaxFiles,"eventMaxFiles"),o(eventMaxFiles,"eventMaxFiles"),s(eventMaxFiles,"eventMaxFiles");const b=1024*1024,a=new Map;export function maybeRotateEventFile(t,e,c){const i=(a.get(t)??0)+e,l=c?.maxBytes??eventMaxBytes(),u=c?.maxFiles??eventMaxFiles(),y=Math.min(b,l);if(i<y){a.set(t,i);return}try{const n=M(t).size;if(a.set(t,0),n<=l)return}catch{a.set(t,i);return}try{E(`${t}.${u}`)}catch{}for(let n=u-1;n>=1;n--)try{v(`${t}.${n}`,`${t}.${n+1}`)}catch{}try{v(t,`${t}.1`)}catch{}}r(maybeRotateEventFile,"maybeRotateEventFile"),o(maybeRotateEventFile,"maybeRotateEventFile"),s(maybeRotateEventFile,"maybeRotateEventFile");export function resetRotationThrottle(t){t===void 0?a.clear():a.delete(t)}r(resetRotationThrottle,"resetRotationThrottle"),o(resetRotationThrottle,"resetRotationThrottle"),s(resetRotationThrottle,"resetRotationThrottle");
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire contract between the per-project dashboard server
|
|
3
|
+
* (`servers/lensmcp-mcp/src/dashboard/*`) and its browser UI (`libs/dashboard-ui`).
|
|
4
|
+
*
|
|
5
|
+
* Plain interfaces only — the UI imports them as types, so nothing here may pull runtime code
|
|
6
|
+
* into the browser bundle. Every shape is additive-only once shipped: an older UI reading a newer
|
|
7
|
+
* server must keep working, so fields are added, never renamed or repurposed.
|
|
8
|
+
*
|
|
9
|
+
* One SSE connection (`GET /api/stream`) carries every live panel as a typed event
|
|
10
|
+
* ({@link DashStreamEvent}); the heavier reads (`/api/flow`, `/api/resource`, `/api/logs`) are
|
|
11
|
+
* on-demand GETs.
|
|
12
|
+
*/
|
|
13
|
+
/** Bumped only on a breaking change to the stream framing itself. */
|
|
14
|
+
export declare const DASHBOARD_PROTOCOL = 1;
|
|
15
|
+
/** The swimlane a flow event belongs to (mirrors `FlowArea` in `@lensmcp/core`'s flow-shape). */
|
|
16
|
+
export type DashFlowArea = 'browser' | 'state' | 'network' | 'gateway' | 'services' | 'data' | 'ai';
|
|
17
|
+
export type DashSeverity = 'debug' | 'info' | 'warning' | 'error' | 'fatal';
|
|
18
|
+
export interface DashWorkspace {
|
|
19
|
+
/** The workspace key — the `/<key>` the gateway mounts this dashboard under. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Human name for the header (the key, or the workspace folder's name). */
|
|
22
|
+
name: string;
|
|
23
|
+
/** The mount prefix, e.g. `/test-lensmcp`; empty when served at `/`. */
|
|
24
|
+
base: string;
|
|
25
|
+
}
|
|
26
|
+
/** First frame on every stream connection. */
|
|
27
|
+
export interface DashHello {
|
|
28
|
+
protocol: number;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
startedAt: number;
|
|
31
|
+
/** Whether `/api/flow?raw=1` is allowed (loopback bind only). */
|
|
32
|
+
rawAllowed: boolean;
|
|
33
|
+
workspace: DashWorkspace;
|
|
34
|
+
/** The log ring's current sequence — resume `/api/logs?since=` from here. */
|
|
35
|
+
logSeq: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether the dev gateway in front of this workspace is serving.
|
|
39
|
+
*
|
|
40
|
+
* `role` matters for the reading: a workspace that is a GUEST on another workspace's `:443`
|
|
41
|
+
* daemon never sees that daemon's `gateway-up` event (it lands on the owner's bus), so liveness
|
|
42
|
+
* there is established from the process tree, not from the event stream.
|
|
43
|
+
*/
|
|
44
|
+
export interface DashGatewayLiveness {
|
|
45
|
+
state: 'live' | 'down' | 'unknown';
|
|
46
|
+
role: 'gateway-child' | 'standalone';
|
|
47
|
+
ports: number[];
|
|
48
|
+
/** When it was last seen up (epoch ms), if known. */
|
|
49
|
+
since?: number;
|
|
50
|
+
/** One line on HOW this was decided — shown in the tooltip, so the verdict is auditable. */
|
|
51
|
+
evidence: string;
|
|
52
|
+
}
|
|
53
|
+
export interface DashSetupIssue {
|
|
54
|
+
id: string;
|
|
55
|
+
title: string;
|
|
56
|
+
detail: string;
|
|
57
|
+
/** A shell command that fixes it, when there is one. */
|
|
58
|
+
fix?: string;
|
|
59
|
+
}
|
|
60
|
+
export type DashSessionPhase = 'empty' | 'live' | 'setup-incomplete';
|
|
61
|
+
export interface DashSession {
|
|
62
|
+
phase: DashSessionPhase;
|
|
63
|
+
gateway: DashGatewayLiveness;
|
|
64
|
+
/** `agent://current-status`'s verdict — the same one the coding agent reads. */
|
|
65
|
+
status: 'starting' | 'clean' | 'warning' | 'failing';
|
|
66
|
+
/** One sentence answering "is anything wrong right now?". */
|
|
67
|
+
headline: string;
|
|
68
|
+
flowCount: number;
|
|
69
|
+
eventCount: number;
|
|
70
|
+
setupIssues: DashSetupIssue[];
|
|
71
|
+
}
|
|
72
|
+
export type DashServiceState = 'up' | 'starting' | 'idle' | 'down' | 'wedged' | 'crash-looping' | 'unknown';
|
|
73
|
+
/** Aggregates over a sliding window (see `DashKpis.windowSec`). `null` = no samples. */
|
|
74
|
+
export interface DashWindowStats {
|
|
75
|
+
count: number;
|
|
76
|
+
errors: number;
|
|
77
|
+
/** errors / count over the window, 0..1 — `null` when there were no requests. */
|
|
78
|
+
errorRate: number | null;
|
|
79
|
+
p50: number | null;
|
|
80
|
+
p95: number | null;
|
|
81
|
+
avgMs: number | null;
|
|
82
|
+
/** Latency samples behind p50/p95 — may be fewer than `count` (see `sampled`). */
|
|
83
|
+
samples: number;
|
|
84
|
+
/**
|
|
85
|
+
* The percentiles come from the requests that carried lens headers (a subset of `count`),
|
|
86
|
+
* not from every proxied request.
|
|
87
|
+
*/
|
|
88
|
+
sampled: boolean;
|
|
89
|
+
}
|
|
90
|
+
export interface DashService {
|
|
91
|
+
project: string;
|
|
92
|
+
/** eager = always warm · lazy = scales from zero · web = a frontend dev server (no pods). */
|
|
93
|
+
kind: 'eager' | 'lazy' | 'web';
|
|
94
|
+
state: DashServiceState;
|
|
95
|
+
/** Plain-language reason for a non-`up` state ("idle — scaled to zero after 10m"). */
|
|
96
|
+
stateReason?: string;
|
|
97
|
+
host?: string;
|
|
98
|
+
path?: string;
|
|
99
|
+
internalHost?: string;
|
|
100
|
+
port?: number;
|
|
101
|
+
internalPort?: number;
|
|
102
|
+
pods: {
|
|
103
|
+
running: number;
|
|
104
|
+
names: string[];
|
|
105
|
+
min?: number;
|
|
106
|
+
max?: number;
|
|
107
|
+
};
|
|
108
|
+
workers: string[];
|
|
109
|
+
lastColdStartMs?: number;
|
|
110
|
+
restarts: number;
|
|
111
|
+
buildFailed: boolean;
|
|
112
|
+
/** Last build/runtime error lines the gateway saw from this service's pod output. */
|
|
113
|
+
errorLines: string[];
|
|
114
|
+
auth?: {
|
|
115
|
+
default?: string;
|
|
116
|
+
overrides: number;
|
|
117
|
+
};
|
|
118
|
+
/** Public traffic into this service since the dashboard started. */
|
|
119
|
+
traffic: {
|
|
120
|
+
count: number;
|
|
121
|
+
errors: number;
|
|
122
|
+
avgMs: number | null;
|
|
123
|
+
lastSeenAt?: number;
|
|
124
|
+
};
|
|
125
|
+
/** Windowed stats + a request sparkline (per `DashKpis.sparkBucketSec`). */
|
|
126
|
+
window?: DashWindowStats & {
|
|
127
|
+
req60: number;
|
|
128
|
+
spark: number[];
|
|
129
|
+
};
|
|
130
|
+
/** Truncated sha256 of the east-west key — never the key. */
|
|
131
|
+
keyFingerprint?: string;
|
|
132
|
+
lastEvent?: {
|
|
133
|
+
at: number;
|
|
134
|
+
title: string;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export interface DashRoute {
|
|
138
|
+
host: string;
|
|
139
|
+
project: string;
|
|
140
|
+
count: number;
|
|
141
|
+
errors: number;
|
|
142
|
+
avgMs: number | null;
|
|
143
|
+
lastSeenAt?: number;
|
|
144
|
+
}
|
|
145
|
+
/** Service → service (east-west) traffic. */
|
|
146
|
+
export interface DashEdge {
|
|
147
|
+
caller: string;
|
|
148
|
+
callee: string;
|
|
149
|
+
host: string;
|
|
150
|
+
count: number;
|
|
151
|
+
errors: number;
|
|
152
|
+
avgMs: number | null;
|
|
153
|
+
lastSeenAt: number;
|
|
154
|
+
}
|
|
155
|
+
export type DashBackendKind = 'db' | 'redis' | 'queue' | 'external' | 'fs' | 'exec';
|
|
156
|
+
export interface DashBackend {
|
|
157
|
+
id: string;
|
|
158
|
+
kind: DashBackendKind;
|
|
159
|
+
name: string;
|
|
160
|
+
usedBy: string[];
|
|
161
|
+
ops: number;
|
|
162
|
+
lastSeenAt: number;
|
|
163
|
+
/** BullMQ-style counts + recent jobs, for `kind: 'queue'`. */
|
|
164
|
+
queue?: {
|
|
165
|
+
counts: Record<string, number>;
|
|
166
|
+
recent: Array<{
|
|
167
|
+
id: string;
|
|
168
|
+
name?: string;
|
|
169
|
+
state?: string;
|
|
170
|
+
at?: number;
|
|
171
|
+
}>;
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
export interface DashHostCheck {
|
|
175
|
+
host: string;
|
|
176
|
+
v4: boolean;
|
|
177
|
+
v6: boolean;
|
|
178
|
+
}
|
|
179
|
+
export interface DashCluster {
|
|
180
|
+
at: number;
|
|
181
|
+
gateway: DashGatewayLiveness;
|
|
182
|
+
services: DashService[];
|
|
183
|
+
routes: DashRoute[];
|
|
184
|
+
edges: DashEdge[];
|
|
185
|
+
backends: DashBackend[];
|
|
186
|
+
hosts: DashHostCheck[];
|
|
187
|
+
/** Appends the missing /etc/hosts lines — present only when a host does not resolve. */
|
|
188
|
+
hostsFix?: string;
|
|
189
|
+
trustCommand: string;
|
|
190
|
+
recent: Array<{
|
|
191
|
+
at: number;
|
|
192
|
+
title: string;
|
|
193
|
+
severity: string;
|
|
194
|
+
}>;
|
|
195
|
+
}
|
|
196
|
+
export interface DashKpis {
|
|
197
|
+
at: number;
|
|
198
|
+
/** The window `last` covers, in seconds. */
|
|
199
|
+
windowSec: number;
|
|
200
|
+
/** Requests through the gateway in the last 60 s. */
|
|
201
|
+
req60: number;
|
|
202
|
+
last: DashWindowStats;
|
|
203
|
+
sparkBucketSec: number;
|
|
204
|
+
/** Oldest → newest. `p95` holds `null` for buckets without latency samples. */
|
|
205
|
+
spark: {
|
|
206
|
+
req: number[];
|
|
207
|
+
errors: number[];
|
|
208
|
+
p95: Array<number | null>;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export type DashWorst = 'ok' | 'warn' | 'error';
|
|
212
|
+
export interface DashFlowSummary {
|
|
213
|
+
flowId: string;
|
|
214
|
+
originType?: string;
|
|
215
|
+
originNodeId?: string;
|
|
216
|
+
startedAt: number;
|
|
217
|
+
lastSeenAt: number;
|
|
218
|
+
eventCount: number;
|
|
219
|
+
/** Human headline: `click · app.tsx:41 → GET /api/storefront`. */
|
|
220
|
+
headline: string;
|
|
221
|
+
/** What started it, in words: `click`, `page load /lab`, `submit`… */
|
|
222
|
+
originLabel: string;
|
|
223
|
+
/** Where in the source it started (`app.tsx:41`), when known. */
|
|
224
|
+
originWhere?: string;
|
|
225
|
+
/** The first request the flow made, if any. */
|
|
226
|
+
request?: {
|
|
227
|
+
method: string;
|
|
228
|
+
path: string;
|
|
229
|
+
status?: number;
|
|
230
|
+
};
|
|
231
|
+
worst: DashWorst;
|
|
232
|
+
/** Wall time of the ACTIVE part (idle tails like pool bookkeeping excluded). */
|
|
233
|
+
durationMs: number;
|
|
234
|
+
/** Event counts per swimlane — drives the lane-mix bar. */
|
|
235
|
+
lanes: Partial<Record<DashFlowArea, number>>;
|
|
236
|
+
/** Services the flow reached. */
|
|
237
|
+
projects: string[];
|
|
238
|
+
/** Bumps whenever an event for this flow arrives — refetch the detail when it changes. */
|
|
239
|
+
rev: number;
|
|
240
|
+
/** How many requests the flow sent through the gateway (a scripted flow can send many). */
|
|
241
|
+
requestCount?: number;
|
|
242
|
+
/** Error-level steps in the flow, and the first one's title — so a list can say WHAT failed. */
|
|
243
|
+
errorCount?: number;
|
|
244
|
+
firstError?: string;
|
|
245
|
+
/**
|
|
246
|
+
* What the flow touched: `GET /route`, `sql:<project>:<op table>`, `egress:<project>:<host>`.
|
|
247
|
+
* How a finding that carries no event ids (a slow route, a slow query shape, a failing host)
|
|
248
|
+
* still finds a flow that shows it.
|
|
249
|
+
*/
|
|
250
|
+
touches?: string[];
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* One display node of a flow — the output of `buildFlowDetail` in `@lensmcp/core`. Every field
|
|
254
|
+
* beyond the first block is optional: which appear depends on the event kind and on the folds
|
|
255
|
+
* (fetch pairing, React commits, ×N repetition) the transform applied.
|
|
256
|
+
*/
|
|
257
|
+
export interface DashFlowNode {
|
|
258
|
+
id: string;
|
|
259
|
+
t: number;
|
|
260
|
+
start: number;
|
|
261
|
+
source: string;
|
|
262
|
+
category: string;
|
|
263
|
+
severity: string;
|
|
264
|
+
title: string;
|
|
265
|
+
kind?: string;
|
|
266
|
+
area: DashFlowArea;
|
|
267
|
+
parent?: string;
|
|
268
|
+
group?: 'browser' | 'server' | string;
|
|
269
|
+
requestId?: string;
|
|
270
|
+
parentRequestId?: string;
|
|
271
|
+
fetchStartId?: string;
|
|
272
|
+
fetchDoneId?: string;
|
|
273
|
+
project?: string;
|
|
274
|
+
role?: string;
|
|
275
|
+
queue?: string;
|
|
276
|
+
jobId?: string;
|
|
277
|
+
sig?: string;
|
|
278
|
+
urlKey?: string;
|
|
279
|
+
durationMs?: number;
|
|
280
|
+
detail?: string;
|
|
281
|
+
steps?: string[];
|
|
282
|
+
ai?: Record<string, unknown>;
|
|
283
|
+
commitId?: string;
|
|
284
|
+
component?: string;
|
|
285
|
+
phase?: string;
|
|
286
|
+
why?: string[];
|
|
287
|
+
viewPhase?: 'mount' | 'update';
|
|
288
|
+
cause?: string;
|
|
289
|
+
causeComponents?: string[];
|
|
290
|
+
components?: string[];
|
|
291
|
+
mounted?: string[];
|
|
292
|
+
renderCount?: number;
|
|
293
|
+
recordedRenders?: number;
|
|
294
|
+
effects?: number;
|
|
295
|
+
effectHooks?: string[];
|
|
296
|
+
count?: number;
|
|
297
|
+
parallel?: boolean;
|
|
298
|
+
avgMs?: number;
|
|
299
|
+
wallMs?: number;
|
|
300
|
+
items?: Array<{
|
|
301
|
+
start: number;
|
|
302
|
+
durationMs?: number;
|
|
303
|
+
}>;
|
|
304
|
+
memberIds?: string[];
|
|
305
|
+
selfMs?: number;
|
|
306
|
+
}
|
|
307
|
+
export interface DashFlowDetail {
|
|
308
|
+
flowId: string;
|
|
309
|
+
/** The reducer's own summary (kept as-is for older consumers). */
|
|
310
|
+
summary: unknown;
|
|
311
|
+
events: DashFlowNode[];
|
|
312
|
+
/** Same counter as `DashFlowSummary.rev` at the time of the read. */
|
|
313
|
+
rev?: number;
|
|
314
|
+
}
|
|
315
|
+
export interface DashLogLine {
|
|
316
|
+
seq: number;
|
|
317
|
+
at: number;
|
|
318
|
+
/** The concrete service (pod project) when known, else the event source. */
|
|
319
|
+
source: string;
|
|
320
|
+
category: string;
|
|
321
|
+
severity: string;
|
|
322
|
+
title: string;
|
|
323
|
+
message?: string;
|
|
324
|
+
flowId?: string;
|
|
325
|
+
}
|
|
326
|
+
export interface DashLogsFrame {
|
|
327
|
+
seq: number;
|
|
328
|
+
sources: string[];
|
|
329
|
+
lines: DashLogLine[];
|
|
330
|
+
}
|
|
331
|
+
export type DashProblemSeverity = 'high' | 'medium' | 'low';
|
|
332
|
+
export type DashProblemKind = 'n-plus-one' | 'db-in-loop' | 'render-storm' | 'slow-path' | 'slow-route' | 'slow-query' | 'missing-index' | 'leak' | 'server-error' | 'browser-error' | 'failed-span' | 'egress-failing' | 'egress-slow' | 'duplicate-requests' | 'build' | 'typecheck' | 'lint' | 'service' | 'other';
|
|
333
|
+
export interface DashProblem {
|
|
334
|
+
/** Stable across updates — the card's identity for the resolved-lifecycle. */
|
|
335
|
+
id: string;
|
|
336
|
+
kind: DashProblemKind;
|
|
337
|
+
severity: DashProblemSeverity;
|
|
338
|
+
/** What is wrong, in one line. */
|
|
339
|
+
title: string;
|
|
340
|
+
/** Where: a route, a query shape, a component, `file:line`. */
|
|
341
|
+
where?: string;
|
|
342
|
+
/** Why it matters / the measurement behind it. */
|
|
343
|
+
detail?: string;
|
|
344
|
+
/** What to do — only when the detector itself knows; never invented. */
|
|
345
|
+
fix?: string;
|
|
346
|
+
/** The concrete artefact: the SQL, the stack's first frames, the failing URL. */
|
|
347
|
+
code?: string;
|
|
348
|
+
/** How many times it happened, when the detector counts. */
|
|
349
|
+
count?: number;
|
|
350
|
+
/** The flow (and node inside it) that shows this happening. */
|
|
351
|
+
evidence?: {
|
|
352
|
+
flowId?: string;
|
|
353
|
+
nodeId?: string;
|
|
354
|
+
eventIds?: string[];
|
|
355
|
+
};
|
|
356
|
+
/** The resource the finding came from — the Raw drawer opens it. */
|
|
357
|
+
resource: string;
|
|
358
|
+
firstSeenAt: number;
|
|
359
|
+
lastSeenAt: number;
|
|
360
|
+
/** Set once the detector stops reporting it — the "did my fix work?" answer. */
|
|
361
|
+
resolvedAt?: number;
|
|
362
|
+
}
|
|
363
|
+
export type DashCheckStatus = 'pass' | 'fail' | 'warn' | 'unchecked' | 'stale';
|
|
364
|
+
/** The build/typecheck/lint/browser signal health row — "not watching" is not "passing". */
|
|
365
|
+
export interface DashCheck {
|
|
366
|
+
id: string;
|
|
367
|
+
label: string;
|
|
368
|
+
status: DashCheckStatus;
|
|
369
|
+
detail: string;
|
|
370
|
+
resource: string;
|
|
371
|
+
}
|
|
372
|
+
export interface DashMemoryOwner {
|
|
373
|
+
ownerId: string;
|
|
374
|
+
itemCount: number;
|
|
375
|
+
growth: number;
|
|
376
|
+
mutations: number;
|
|
377
|
+
lastMutatedAt?: number;
|
|
378
|
+
suspected: boolean;
|
|
379
|
+
flowIds: string[];
|
|
380
|
+
}
|
|
381
|
+
export interface DashHotComponent {
|
|
382
|
+
component: string;
|
|
383
|
+
renders: number;
|
|
384
|
+
totalMs: number;
|
|
385
|
+
}
|
|
386
|
+
export interface DashDiagnostics {
|
|
387
|
+
at: number;
|
|
388
|
+
problems: DashProblem[];
|
|
389
|
+
checks: DashCheck[];
|
|
390
|
+
memory: {
|
|
391
|
+
heapUsed?: number;
|
|
392
|
+
rss?: number;
|
|
393
|
+
owners: DashMemoryOwner[];
|
|
394
|
+
};
|
|
395
|
+
react: {
|
|
396
|
+
hot: DashHotComponent[];
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* server — a request a service answered with a 5xx / an exception reached its handler
|
|
401
|
+
* exception — a traced call threw, but the request survived
|
|
402
|
+
* request — a request failed before or outside a service (gateway 5xx, browser fetch failure)
|
|
403
|
+
* browser — an uncaught error / rejected promise / failed resource in the page
|
|
404
|
+
* external — a call to a third-party host failed
|
|
405
|
+
* queue — a background job failed
|
|
406
|
+
* log — an error-level log line no request explains
|
|
407
|
+
* build / typecheck / service — the code or a service is broken right now (standing, not events)
|
|
408
|
+
*/
|
|
409
|
+
export type DashErrorKind = 'server' | 'exception' | 'request' | 'browser' | 'external' | 'queue' | 'log' | 'build' | 'typecheck' | 'service';
|
|
410
|
+
export interface DashErrorOccurrence {
|
|
411
|
+
/** The primary event's id. */
|
|
412
|
+
id: string;
|
|
413
|
+
at: number;
|
|
414
|
+
flowId?: string;
|
|
415
|
+
/** The deepest failing step inside the flow — where the trace should open. */
|
|
416
|
+
stepId?: string;
|
|
417
|
+
requestId?: string;
|
|
418
|
+
status?: number;
|
|
419
|
+
/** Only when it differs from the group's message. */
|
|
420
|
+
message?: string;
|
|
421
|
+
}
|
|
422
|
+
export interface DashErrorGroup {
|
|
423
|
+
/** Stable signature — the same error, grouped however many times it happens. */
|
|
424
|
+
id: string;
|
|
425
|
+
kind: DashErrorKind;
|
|
426
|
+
/** What failed: `GET /orders/lab/boom → 500`, `Uncaught Error: …`. */
|
|
427
|
+
title: string;
|
|
428
|
+
/** Where: the service, host or page. */
|
|
429
|
+
where?: string;
|
|
430
|
+
project?: string;
|
|
431
|
+
message?: string;
|
|
432
|
+
/** App frames first; framework frames trimmed. */
|
|
433
|
+
stack?: string;
|
|
434
|
+
status?: number;
|
|
435
|
+
count: number;
|
|
436
|
+
firstSeenAt: number;
|
|
437
|
+
lastSeenAt: number;
|
|
438
|
+
/** Newest first, bounded. */
|
|
439
|
+
occurrences: DashErrorOccurrence[];
|
|
440
|
+
/** Distinct flows it happened in. */
|
|
441
|
+
flowCount: number;
|
|
442
|
+
/** Occurrences per minute, oldest → newest (last 30 minutes). */
|
|
443
|
+
spark: number[];
|
|
444
|
+
/** A standing condition (a broken build, a crashing service) rather than an event. */
|
|
445
|
+
ongoing?: boolean;
|
|
446
|
+
/** The resource this came from, for the Raw drawer. */
|
|
447
|
+
resource?: string;
|
|
448
|
+
}
|
|
449
|
+
export interface DashErrors {
|
|
450
|
+
at: number;
|
|
451
|
+
/** What "recent" means below (ms). */
|
|
452
|
+
recentMs: number;
|
|
453
|
+
groups: DashErrorGroup[];
|
|
454
|
+
/** Groups seen within `recentMs` (plus ongoing ones). */
|
|
455
|
+
recentGroups: number;
|
|
456
|
+
recentOccurrences: number;
|
|
457
|
+
}
|
|
458
|
+
export interface DashResourceEntry {
|
|
459
|
+
uri: string;
|
|
460
|
+
scheme: string;
|
|
461
|
+
revision?: number;
|
|
462
|
+
}
|
|
463
|
+
export interface DashFlowRev {
|
|
464
|
+
flowId: string;
|
|
465
|
+
rev: number;
|
|
466
|
+
}
|
|
467
|
+
/** Every SSE frame on `/api/stream`: the SSE `event:` name is `type`, `data:` is `data`. */
|
|
468
|
+
export type DashStreamEvent = {
|
|
469
|
+
type: 'hello';
|
|
470
|
+
data: DashHello;
|
|
471
|
+
} | {
|
|
472
|
+
type: 'session';
|
|
473
|
+
data: DashSession;
|
|
474
|
+
} | {
|
|
475
|
+
type: 'cluster';
|
|
476
|
+
data: DashCluster;
|
|
477
|
+
} | {
|
|
478
|
+
type: 'kpis';
|
|
479
|
+
data: DashKpis;
|
|
480
|
+
} | {
|
|
481
|
+
type: 'flows';
|
|
482
|
+
data: {
|
|
483
|
+
flows: DashFlowSummary[];
|
|
484
|
+
};
|
|
485
|
+
} | {
|
|
486
|
+
type: 'flow-rev';
|
|
487
|
+
data: DashFlowRev[];
|
|
488
|
+
} | {
|
|
489
|
+
type: 'logs';
|
|
490
|
+
data: DashLogsFrame;
|
|
491
|
+
}
|
|
492
|
+
/** The server dropped log frames for this client (slow socket) — refetch from `seq`. */
|
|
493
|
+
| {
|
|
494
|
+
type: 'logs-resync';
|
|
495
|
+
data: {
|
|
496
|
+
seq: number;
|
|
497
|
+
};
|
|
498
|
+
} | {
|
|
499
|
+
type: 'diagnostics';
|
|
500
|
+
data: DashDiagnostics;
|
|
501
|
+
} | {
|
|
502
|
+
type: 'errors';
|
|
503
|
+
data: DashErrors;
|
|
504
|
+
};
|
|
505
|
+
export type DashStreamEventType = DashStreamEvent['type'];
|
package/lib/dashboard.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";export const DASHBOARD_PROTOCOL=1;
|
package/lib/flood-guard.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var S=Object.defineProperty;var i=(t,s)=>S(t,"name",{value:s,configurable:!0});var A=Object.defineProperty,f=i((t,s)=>A(t,"name",{value:s,configurable:!0}),"i"),k=Object.defineProperty,u=f((t,s)=>k(t,"name",{value:s,configurable:!0}),"o");function l(){const t=typeof process<"u"?process.env?.LENSMCP_EVENT_MAX_EPS:void 0;if(t===void 0||t==="")return;if(t==="off")return"off";const s=Number(t);if(Number.isFinite(s))return s<=0?"off":s}i(l,"p"),f(l,"S"),u(l,"envPerSec");export function createEventFloodGuard(t){const s=l();if(s==="off")return{admit:u(()=>({ok:!0,suppressed:0}),"admit")};const c=s??t?.perSec??5,d=Math.max(1,t?.burst??Math.max(30,c*4)),p=t?.maxKeys??2048,n=new Map,v=u(o=>{for(const[r,e]of n)o-e.lastSeenAt>6e4&&n.delete(r);if(n.size>p){const r=[...n.entries()].sort((e,a)=>e[1].lastSeenAt-a[1].lastSeenAt);for(let e=0;e<r.length>>1;e++)n.delete(r[e][0])}},"evictStale");return{admit(o){const r=Date.now();let e=n.get(o);e||(n.size>=p&&v(r),e={tokens:d,lastRefillAt:r,suppressed:0,lastSeenAt:r},n.set(o,e)),e.lastSeenAt=r;const a=r-e.lastRefillAt;if(a>0&&(e.tokens=Math.min(d,e.tokens+a/1e3*c),e.lastRefillAt=r),e.tokens>=1){e.tokens-=1;const m=e.suppressed;return e.suppressed=0,{ok:!0,suppressed:m}}return e.suppressed+=1,{ok:!1,suppressed:0}}}}i(createEventFloodGuard,"createEventFloodGuard"),f(createEventFloodGuard,"createEventFloodGuard"),u(createEventFloodGuard,"createEventFloodGuard");
|
package/lib/producer-health.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var R=Object.defineProperty;var i=(e,o)=>R(e,"name",{value:o,configurable:!0});var $=Object.defineProperty,d=i((e,o)=>$(e,"name",{value:o,configurable:!0}),"i"),b=Object.defineProperty,m=d((e,o)=>b(e,"name",{value:o,configurable:!0}),"u");import{z as r}from"zod";export const ProducerStateSchema=r.enum(["none","connected"]),ProducerRunStatusSchema=r.enum(["running","clean","failing","error"]),ProducerReportSchema=r.object({id:r.string(),tool:r.string(),project:r.string().optional(),firstReportedAt:r.number().int().nonnegative(),lastReportedAt:r.number().int().nonnegative(),lastRunStatus:ProducerRunStatusSchema,lastRunDurationMs:r.number().int().nonnegative().optional(),errorCount:r.number().int().nonnegative(),warningCount:r.number().int().nonnegative().optional(),error:r.string().optional()}),ProducerHealthSchema=r.object({producer:ProducerStateSchema,producers:r.array(ProducerReportSchema),lastReportedAt:r.number().int().nonnegative().optional(),ageMs:r.number().int().nonnegative().optional(),stale:r.boolean(),detail:r.string()}),PRODUCER_STALE_AFTER_MS=10*6e4;export function summariseProducers(e){const o=e.now??Date.now(),t=[...e.producers].sort((n,h)=>h.lastReportedAt-n.lastReportedAt);if(t.length===0)return{producer:"none",producers:[],stale:!1,detail:`No ${e.domain} producer has ever reported into this session \u2014 this is NOT a clean result, nothing is watching. ${e.noProducerHint}`};const l=t[0].lastReportedAt,u=Math.max(0,o-l),p=u>PRODUCER_STALE_AFTER_MS,g=t.filter(n=>n.lastRunStatus==="failing"),s=t.filter(n=>n.lastRunStatus==="error"),a=[`${t.length} ${e.domain} producer${t.length===1?"":"s"} reporting (${t.map(n=>n.id).join(", ")}); newest report ${c(u)}.`];return s.length>0&&a.push(`${s.length} produced NO verdict (the tool itself failed): ${s.map(n=>`${n.id} \u2014 ${n.error??"unknown error"}`).join("; ")}.`),g.length>0&&a.push(`${g.length} reporting errors.`),p&&a.push(`Older than ${Math.round(PRODUCER_STALE_AFTER_MS/6e4)}m \u2014 the verdict may predate the current source.`),{producer:"connected",producers:t,lastReportedAt:l,ageMs:u,stale:p,detail:a.join(" ")}}i(summariseProducers,"summariseProducers"),d(summariseProducers,"summariseProducers"),m(summariseProducers,"summariseProducers");function c(e){return e<1e3?"just now":e<6e4?`${Math.round(e/1e3)}s ago`:e<36e5?`${Math.round(e/6e4)}m ago`:`${Math.round(e/36e5)}h ago`}i(c,"g"),d(c,"g"),m(c,"describeAge");
|