@lensmcp/protocol-types 1.25.5 → 1.25.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.d.ts +2 -0
- package/index.js +1 -1
- package/lib/dashboard.d.ts +413 -7
- package/lib/dashboard.js +1 -1
- package/lib/http-exchange.d.ts +8 -4
- package/lib/page-visit.d.ts +136 -0
- package/lib/page-visit.js +1 -0
- package/lib/sources.d.ts +225 -0
- package/lib/sources.js +1 -0
- 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";export*from"./lib/dashboard.js";export*from"./lib/log-query.js";export*from"./lib/error-dismissal.js";export*from"./lib/ai-content.js";export*from"./lib/http-exchange.js";export*from"./lib/pod-memory.js";export*from"./lib/perf-rules.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";export*from"./lib/log-query.js";export*from"./lib/error-dismissal.js";export*from"./lib/ai-content.js";export*from"./lib/http-exchange.js";export*from"./lib/pod-memory.js";export*from"./lib/perf-rules.js";export*from"./lib/sources.js";export*from"./lib/page-visit.js";
|
package/lib/dashboard.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* on-demand GETs.
|
|
12
12
|
*/
|
|
13
13
|
import type { HttpExchange } from './http-exchange.js';
|
|
14
|
+
import type { PageVisitNavType, PageVisitRaw, PageVisitReadyBy, PageVisitRequest, PageVisitVitals } from './page-visit.js';
|
|
14
15
|
/** Bumped only on a breaking change to the stream framing itself. */
|
|
15
16
|
export declare const DASHBOARD_PROTOCOL = 1;
|
|
16
17
|
/** The swimlane a flow event belongs to (mirrors `FlowArea` in `@lensmcp/core`'s flow-shape). */
|
|
@@ -521,6 +522,14 @@ export interface DashFlowDetail {
|
|
|
521
522
|
segmentStats?: DashSegmentStats;
|
|
522
523
|
/** Present when the read asked for one segment (`?segment=`): `events` then holds only that segment. */
|
|
523
524
|
focus?: DashFlowSegment;
|
|
525
|
+
/**
|
|
526
|
+
* Present when the read asked for a time window (`?from=&to=`, epoch ms — a page visit's start and ready): `events`
|
|
527
|
+
* then holds only the flow's steps inside it, while `segments`/`phases` still describe the whole flow.
|
|
528
|
+
*/
|
|
529
|
+
window?: {
|
|
530
|
+
from?: number;
|
|
531
|
+
to?: number;
|
|
532
|
+
};
|
|
524
533
|
/**
|
|
525
534
|
* Present when the flow was no longer in the live window and was read back — from the workspace's event history
|
|
526
535
|
* (30 days) or the bus files. `complete: false`: older events of it may be missing.
|
|
@@ -656,6 +665,19 @@ export interface DashDiagnostics {
|
|
|
656
665
|
slow: number;
|
|
657
666
|
warnings: number;
|
|
658
667
|
};
|
|
668
|
+
/**
|
|
669
|
+
* Set on the stream's frame, which carries only what a page lists: the findings of a {@link DASH_PERF_KINDS} kind
|
|
670
|
+
* (failures are the Errors screen's, from `DashErrors`), the open ones and then the recently resolved ones, each up
|
|
671
|
+
* to a cap. These are their numbers before the cap — `open - problems.filter(open).length` is a list's «N more».
|
|
672
|
+
* Absent from older servers, whose frame held every problem.
|
|
673
|
+
*/
|
|
674
|
+
totals?: {
|
|
675
|
+
open: number;
|
|
676
|
+
resolved: number;
|
|
677
|
+
high: number;
|
|
678
|
+
medium: number;
|
|
679
|
+
low: number;
|
|
680
|
+
};
|
|
659
681
|
}
|
|
660
682
|
/** The problem kinds that are "slow or wasteful" — Performance's findings and the slow counter. */
|
|
661
683
|
export declare const DASH_PERF_KINDS: readonly DashProblemKind[];
|
|
@@ -742,6 +764,11 @@ export interface DashErrors {
|
|
|
742
764
|
recentOccurrences: number;
|
|
743
765
|
/** Hidden by hand, and still hidden: nothing of them has been seen since. Listed so they can be restored. */
|
|
744
766
|
dismissed: DashErrorGroup[];
|
|
767
|
+
/**
|
|
768
|
+
* Set on the stream's frame: each group carries at most this many `occurrences`, newest first (and the newest with a
|
|
769
|
+
* flow when none of those has one) — what Errors shows of a group. The error log keeps more; `count` is all of them.
|
|
770
|
+
*/
|
|
771
|
+
occurrencesShown?: number;
|
|
745
772
|
}
|
|
746
773
|
/** `POST /api/errors/dismiss` and `/api/errors/restore` — the body, and the answer. */
|
|
747
774
|
export interface DashErrorIds {
|
|
@@ -798,8 +825,55 @@ export type DashStreamEvent = {
|
|
|
798
825
|
} | {
|
|
799
826
|
type: 'errors';
|
|
800
827
|
data: DashErrors;
|
|
828
|
+
}
|
|
829
|
+
/** The shell's numbers, on every tab whatever page it shows. */
|
|
830
|
+
| {
|
|
831
|
+
type: 'summary';
|
|
832
|
+
data: DashSummary;
|
|
833
|
+
}
|
|
834
|
+
/** Every 15 s on every connection — what tells a tab its stream is alive when its page's frames are quiet. */
|
|
835
|
+
| {
|
|
836
|
+
type: 'ping';
|
|
837
|
+
data: Record<string, never>;
|
|
801
838
|
};
|
|
802
839
|
export type DashStreamEventType = DashStreamEvent['type'];
|
|
840
|
+
/**
|
|
841
|
+
* What a tab asks the stream for: `GET /api/stream?topics=cluster,flows` — the frames its page reads. `hello` and
|
|
842
|
+
* `ping` go to every tab, `summary` too (asked or not); `logs-resync` goes with `logs`, and the log backlog is sent
|
|
843
|
+
* only to a tab that asks for `logs`. A tab that sends no `topics` gets every frame (a UI from before topics).
|
|
844
|
+
*/
|
|
845
|
+
export type DashStreamTopic = 'summary' | 'session' | 'cluster' | 'kpis' | 'flows' | 'flow-rev' | 'logs' | 'diagnostics' | 'errors';
|
|
846
|
+
export declare const DASH_STREAM_TOPICS: readonly DashStreamTopic[];
|
|
847
|
+
/**
|
|
848
|
+
* The shell's numbers — the sidebar's badges, the top bar's counters, the scope card — for a tab whose page does not
|
|
849
|
+
* take the frame they come from. Counted by the same code as those frames, from what their producers last built.
|
|
850
|
+
* A part is absent until its producer first ran.
|
|
851
|
+
*/
|
|
852
|
+
export interface DashSummary {
|
|
853
|
+
/** `DashErrors.recentGroups`, and how many groups it lists (`groups.length`). */
|
|
854
|
+
errors?: {
|
|
855
|
+
recentGroups: number;
|
|
856
|
+
groups: number;
|
|
857
|
+
};
|
|
858
|
+
/** `DashDiagnostics.counts`, and each warning's title (`warnings[].title`) — the top bar's tooltip. */
|
|
859
|
+
diagnostics?: {
|
|
860
|
+
slow: number;
|
|
861
|
+
warnings: number;
|
|
862
|
+
warningTitles: string[];
|
|
863
|
+
};
|
|
864
|
+
/** Each listed flow's `lastSeenAt`, newest first, as many as `flows` lists: the «new flows» badge counts those after the tab last looked. */
|
|
865
|
+
flows?: {
|
|
866
|
+
lastSeenAt: number[];
|
|
867
|
+
};
|
|
868
|
+
/** Services down, wedged or crash-looping; and the scope card's `DashCluster.instance` and `worktrees`. */
|
|
869
|
+
cluster?: {
|
|
870
|
+
troubled: number;
|
|
871
|
+
instance?: DashCluster['instance'];
|
|
872
|
+
worktrees?: DashWorktree[];
|
|
873
|
+
};
|
|
874
|
+
/** `DashSession.setupIssues.length`. */
|
|
875
|
+
setupIssues?: number;
|
|
876
|
+
}
|
|
803
877
|
/**
|
|
804
878
|
* What one run of a route touched, by key, with how many times:
|
|
805
879
|
* `svc:<callee>` a service it called · `ext:<host>` a host outside the cluster · `sql:<VERB> <table>`
|
|
@@ -807,15 +881,38 @@ export type DashStreamEventType = DashStreamEvent['type'];
|
|
|
807
881
|
* `err:<what>` something that threw.
|
|
808
882
|
*/
|
|
809
883
|
export type DashTouches = Record<string, number>;
|
|
884
|
+
/**
|
|
885
|
+
* A run's SQL by its service's own count (node-instrumentation's `db-request-account`): every statement it ran and what
|
|
886
|
+
* they took, and how many of their events reached the lens. Each statement shape's flood bucket lets only some events
|
|
887
|
+
* through, so a run's `touches` and `sqlMs` can count a fraction of what it ran — this is the whole of it.
|
|
888
|
+
*/
|
|
889
|
+
export interface DashSqlAccount {
|
|
890
|
+
statements: number;
|
|
891
|
+
ms: number;
|
|
892
|
+
/** Statements whose event reached the lens — what `touches` counts (and session statements, BEGIN and the like). */
|
|
893
|
+
seen: number;
|
|
894
|
+
failed?: number;
|
|
895
|
+
/**
|
|
896
|
+
* Folded by time, not by the run's own id: a service on a tap from before `runId` that sent several calls under one
|
|
897
|
+
* request id at once can have had the accounts of two such calls swapped. Their sum is right.
|
|
898
|
+
*/
|
|
899
|
+
byTime?: true;
|
|
900
|
+
}
|
|
810
901
|
export interface DashRouteRun {
|
|
811
902
|
at: number;
|
|
812
903
|
flowId?: string;
|
|
813
904
|
requestId?: string;
|
|
814
905
|
status?: number;
|
|
815
906
|
durationMs?: number;
|
|
907
|
+
/** What its SQL statements took, summed (ms) — the statements its `sql:` touches count. */
|
|
908
|
+
sqlMs?: number;
|
|
909
|
+
/** Its SQL by its service's own count, when the service sent one — the whole of it, where `touches` may hold a part. */
|
|
910
|
+
sqlAccount?: DashSqlAccount;
|
|
816
911
|
/** Who called it: another service by name, else `client`. */
|
|
817
912
|
caller?: string;
|
|
818
913
|
touches: DashTouches;
|
|
914
|
+
/** Its request and response as they came in and went out (an `ingress` exchange, `/api/exchange?id=`). */
|
|
915
|
+
exchangeId?: string;
|
|
819
916
|
}
|
|
820
917
|
export type DashRouteChangeKind = 'new-external' | 'new-service' | 'new-query' | 'new-queue' | 'new-cache' | 'new-error' | 'gone' | 'gone-error' | 'count' | 'status' | 'duration';
|
|
821
918
|
export interface DashRouteChange {
|
|
@@ -1024,6 +1121,201 @@ export interface DashRoutesCatalog {
|
|
|
1024
1121
|
/** How far back the kept history reaches. */
|
|
1025
1122
|
since?: number;
|
|
1026
1123
|
}
|
|
1124
|
+
/** p50 / p95 over a page's kept samples, and how many it has. */
|
|
1125
|
+
export interface DashPageStat {
|
|
1126
|
+
p50: number;
|
|
1127
|
+
p95: number;
|
|
1128
|
+
n: number;
|
|
1129
|
+
}
|
|
1130
|
+
/** One request of a visit, as the browser saw it, joined to the backend's run of it when that was seen. */
|
|
1131
|
+
export interface DashPageRequest extends PageVisitRequest {
|
|
1132
|
+
/** The route that answered it (`<service> METHOD /route`, an id of catalog://routes). */
|
|
1133
|
+
routeId?: string;
|
|
1134
|
+
service?: string;
|
|
1135
|
+
/** The server's own status and time. */
|
|
1136
|
+
status?: number;
|
|
1137
|
+
serverMs?: number;
|
|
1138
|
+
/**
|
|
1139
|
+
* SQL statements, and what they took, over its run and the cluster calls it waited on — by the services' own count
|
|
1140
|
+
* where they sent one (`DashRouteRun.sqlAccount`), else what reached the lens.
|
|
1141
|
+
*/
|
|
1142
|
+
sql?: number;
|
|
1143
|
+
sqlMs?: number;
|
|
1144
|
+
/** Of `sql`, how many statements' events reached the lens — only where that is fewer. */
|
|
1145
|
+
sqlSeen?: number;
|
|
1146
|
+
/** Key-value commands and calls outside the cluster, over the same runs. */
|
|
1147
|
+
cache?: number;
|
|
1148
|
+
ext?: number;
|
|
1149
|
+
/** Other services' runs under its request id, inside its own run (east-west): how many, and by route (at most 10 routes). */
|
|
1150
|
+
callCount?: number;
|
|
1151
|
+
calls?: Array<{
|
|
1152
|
+
routeId: string;
|
|
1153
|
+
n: number;
|
|
1154
|
+
}>;
|
|
1155
|
+
/** Same method, path and query hash as an earlier request of the visit. */
|
|
1156
|
+
dup?: true;
|
|
1157
|
+
}
|
|
1158
|
+
/** One visit of a page, in a few hundred bytes — the requests in full only on the newest two and the slowest. */
|
|
1159
|
+
export interface DashPageVisit {
|
|
1160
|
+
visitId: string;
|
|
1161
|
+
tabId?: string;
|
|
1162
|
+
/** The pathname it opened. */
|
|
1163
|
+
path: string;
|
|
1164
|
+
nav: 'hard' | 'spa';
|
|
1165
|
+
navType?: PageVisitNavType;
|
|
1166
|
+
cause?: 'hmr';
|
|
1167
|
+
redirectedFrom?: string;
|
|
1168
|
+
/** The page the tab showed before (an SPA navigation): its pattern, or its path when it had none. */
|
|
1169
|
+
from?: string;
|
|
1170
|
+
startAt: number;
|
|
1171
|
+
readyAt?: number;
|
|
1172
|
+
readyBy?: PageVisitReadyBy;
|
|
1173
|
+
readyMs?: number;
|
|
1174
|
+
appReadyMs?: number;
|
|
1175
|
+
endAt?: number;
|
|
1176
|
+
flowIds: string[];
|
|
1177
|
+
/** Requests it made until ready, with those past the 60 the browser lists. */
|
|
1178
|
+
requests: number;
|
|
1179
|
+
/** Of the listed ones, how many were joined to their backend run. */
|
|
1180
|
+
joined: number;
|
|
1181
|
+
/** Requests whose run has not been seen yet — joined as it arrives, given up two minutes after the visit. */
|
|
1182
|
+
waiting?: number;
|
|
1183
|
+
/** The same request made again in the visit (method, path and query hash). */
|
|
1184
|
+
duplicates: number;
|
|
1185
|
+
/** Over its joined requests (`DashPageRequest.sql`). */
|
|
1186
|
+
sql: number;
|
|
1187
|
+
sqlMs: number;
|
|
1188
|
+
/** Of `sql`, how many statements' events reached the lens — only where that is fewer. */
|
|
1189
|
+
sqlSeen?: number;
|
|
1190
|
+
/** The joined requests by the service that answered them. */
|
|
1191
|
+
services?: Record<string, number>;
|
|
1192
|
+
commits: number;
|
|
1193
|
+
renders: number;
|
|
1194
|
+
renderMs: number;
|
|
1195
|
+
maxCommitMs: number;
|
|
1196
|
+
stormCommits: number;
|
|
1197
|
+
suspenseWaits: number;
|
|
1198
|
+
updates: number;
|
|
1199
|
+
errors: number;
|
|
1200
|
+
firstError?: string;
|
|
1201
|
+
/** A request answered 5xx, the page threw, or it was never ready (`timeout`). */
|
|
1202
|
+
failed?: true;
|
|
1203
|
+
afterReady?: PageVisitRaw['afterReady'];
|
|
1204
|
+
vitals?: PageVisitVitals;
|
|
1205
|
+
longTasks?: PageVisitRaw['longTasks'];
|
|
1206
|
+
heapMb?: number;
|
|
1207
|
+
chunks?: PageVisitRaw['chunks'];
|
|
1208
|
+
/** Its requests in full — kept for the newest two visits and the slowest. */
|
|
1209
|
+
requestList?: DashPageRequest[];
|
|
1210
|
+
}
|
|
1211
|
+
/** A request the page makes, over its kept visits: `METHOD /path` with ids folded. */
|
|
1212
|
+
export interface DashPageUse {
|
|
1213
|
+
template: string;
|
|
1214
|
+
method: string;
|
|
1215
|
+
path: string;
|
|
1216
|
+
host?: string;
|
|
1217
|
+
/** The backend route that answers it, once a run of it was joined. */
|
|
1218
|
+
routeId?: string;
|
|
1219
|
+
/** In how many visits, and how often per visit that made it. */
|
|
1220
|
+
inVisits: number;
|
|
1221
|
+
perVisit: number;
|
|
1222
|
+
/** The browser's time to its response headers. */
|
|
1223
|
+
p50?: number;
|
|
1224
|
+
/** Calls that answered 4xx/5xx or failed on the network. */
|
|
1225
|
+
failed: number;
|
|
1226
|
+
/** Calls that repeated an earlier one of their visit (same method, path and query hash). */
|
|
1227
|
+
duplicates: number;
|
|
1228
|
+
/** SQL statements per joined call, on average. */
|
|
1229
|
+
sqlPerCall?: number;
|
|
1230
|
+
/** Made by the latest visit. */
|
|
1231
|
+
latest: boolean;
|
|
1232
|
+
}
|
|
1233
|
+
export type DashPageChangeKind = 'new-request' | 'gone-request' | 'status' | 'failed' | 'count' | 'sql' | 'duration';
|
|
1234
|
+
export interface DashPageChange {
|
|
1235
|
+
kind: DashPageChangeKind;
|
|
1236
|
+
/** The request template it is about, when it is about one. */
|
|
1237
|
+
key?: string;
|
|
1238
|
+
/** One line: "now requests GET /api/orders/:id", "GET /api/x ×1 → ×3", "SQL 12 → 31 statements", "ready 420 ms → 1,310 ms". */
|
|
1239
|
+
text: string;
|
|
1240
|
+
}
|
|
1241
|
+
export interface DashPageSummary {
|
|
1242
|
+
/** `<app> <pattern>`. */
|
|
1243
|
+
id: string;
|
|
1244
|
+
/** Its newest finished page review — none until the `page` review exists (planning/pages S6). */
|
|
1245
|
+
review?: DashReviewMark;
|
|
1246
|
+
/** The app the Vite plugin serves (or the page's host). */
|
|
1247
|
+
app: string;
|
|
1248
|
+
/** The router's full match pattern. */
|
|
1249
|
+
pattern: string;
|
|
1250
|
+
firstSeenAt: number;
|
|
1251
|
+
lastSeenAt: number;
|
|
1252
|
+
visits: number;
|
|
1253
|
+
hard: number;
|
|
1254
|
+
spa: number;
|
|
1255
|
+
/** Hard loads the dev server's hot reload caused: listed, kept out of every sample. */
|
|
1256
|
+
hmr: number;
|
|
1257
|
+
errors: number;
|
|
1258
|
+
failedVisits: number;
|
|
1259
|
+
/** Ready per population: SPA visits from the navigation (`readyMs`), hard loads from the app's entry (`appReadyMs`). */
|
|
1260
|
+
ready: {
|
|
1261
|
+
spa?: DashPageStat;
|
|
1262
|
+
hard?: DashPageStat;
|
|
1263
|
+
};
|
|
1264
|
+
/** Per visit: requests, SQL statements and ms, renders and render ms, store updates. */
|
|
1265
|
+
perVisit: Partial<Record<'requests' | 'sql' | 'sqlMs' | 'renders' | 'renderMs' | 'updates', DashPageStat>>;
|
|
1266
|
+
/** The latest visit — not a hot reload, unless the page has nothing else — without its request list. */
|
|
1267
|
+
last?: DashPageVisit;
|
|
1268
|
+
/** Where its SPA visits came from (the tab's page before), most often first; at most 3. */
|
|
1269
|
+
from: Array<{
|
|
1270
|
+
page: string;
|
|
1271
|
+
n: number;
|
|
1272
|
+
}>;
|
|
1273
|
+
/** The latest visit against the one before — hot reloads left out. */
|
|
1274
|
+
changes: DashPageChange[];
|
|
1275
|
+
}
|
|
1276
|
+
export interface DashPageDetail extends DashPageSummary {
|
|
1277
|
+
/** Newest first, at most 20; the newest two carry `requestList`. */
|
|
1278
|
+
recent: DashPageVisit[];
|
|
1279
|
+
/** The slowest visit kept (by its population's ready), with its requests. */
|
|
1280
|
+
slowest?: DashPageVisit;
|
|
1281
|
+
/** What it requests, most visits first; at most 50. */
|
|
1282
|
+
uses: DashPageUse[];
|
|
1283
|
+
/** The most-rendered components, over the visits that reported them; at most 20. */
|
|
1284
|
+
components: Array<{
|
|
1285
|
+
name: string;
|
|
1286
|
+
n: number;
|
|
1287
|
+
ms: number;
|
|
1288
|
+
inVisits: number;
|
|
1289
|
+
}>;
|
|
1290
|
+
/** The most-updated store paths; at most 20. */
|
|
1291
|
+
store: Array<{
|
|
1292
|
+
store: string;
|
|
1293
|
+
path: string;
|
|
1294
|
+
n: number;
|
|
1295
|
+
inVisits: number;
|
|
1296
|
+
}>;
|
|
1297
|
+
/** Real paths that took this pattern, newest first; at most 10. */
|
|
1298
|
+
paths: string[];
|
|
1299
|
+
}
|
|
1300
|
+
/** Visits with no router pattern, by app and templated path: not pages (planning/hub decision 9). */
|
|
1301
|
+
export interface DashPageUnmatched {
|
|
1302
|
+
app: string;
|
|
1303
|
+
path: string;
|
|
1304
|
+
visits: number;
|
|
1305
|
+
lastSeenAt: number;
|
|
1306
|
+
}
|
|
1307
|
+
export interface DashPagesCatalog {
|
|
1308
|
+
at: number;
|
|
1309
|
+
/** Most recently seen first. */
|
|
1310
|
+
pages: DashPageSummary[];
|
|
1311
|
+
/** At most 50 paths; `visits` sums them. */
|
|
1312
|
+
unmatched: {
|
|
1313
|
+
visits: number;
|
|
1314
|
+
paths: DashPageUnmatched[];
|
|
1315
|
+
};
|
|
1316
|
+
/** How far back the kept visits reach. */
|
|
1317
|
+
since?: number;
|
|
1318
|
+
}
|
|
1027
1319
|
/** What one AI model was asked through an endpoint, and what it cost in tokens. */
|
|
1028
1320
|
export interface DashAiUse {
|
|
1029
1321
|
model: string;
|
|
@@ -1051,8 +1343,13 @@ export interface DashExternalCall {
|
|
|
1051
1343
|
/** The request it was made in — the calls that share it are one request's steps. */
|
|
1052
1344
|
requestId?: string;
|
|
1053
1345
|
}
|
|
1054
|
-
/**
|
|
1346
|
+
/**
|
|
1347
|
+
* One exchange opened in the Request & response panel: an outbound call (Data › External APIs, a flow's step) or a
|
|
1348
|
+
* route's run (Routes › a route › Runs) — the call or the run, its exchange, its request's other steps.
|
|
1349
|
+
*/
|
|
1055
1350
|
export interface DashExchangeView {
|
|
1351
|
+
/** What was opened: an outbound call (`host` is its host), or a route's run (`host` is the service that served it). */
|
|
1352
|
+
of?: 'call' | 'run';
|
|
1056
1353
|
host: string;
|
|
1057
1354
|
call: DashExternalCall;
|
|
1058
1355
|
/** Absent: not captured (capture off, headers rotated away) or withheld (`withheld` says why). */
|
|
@@ -1166,6 +1463,14 @@ export interface DashSqlFinding {
|
|
|
1166
1463
|
tool: string;
|
|
1167
1464
|
at: number;
|
|
1168
1465
|
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Why a statement is test SQL: every site it ran from is a test file (`sites`), its name starts with `test.` (`name`), or
|
|
1468
|
+
* nx.json → `lensmcp.sql.test` lists it (`listed`).
|
|
1469
|
+
*/
|
|
1470
|
+
export interface DashSqlTestMark {
|
|
1471
|
+
by: 'sites' | 'name' | 'listed';
|
|
1472
|
+
why: string;
|
|
1473
|
+
}
|
|
1169
1474
|
/**
|
|
1170
1475
|
* A place in the services' own code where something ran — what a coding agent opens. From the stack the tap took at
|
|
1171
1476
|
* the call: the service's nearest frame, and the callers above it.
|
|
@@ -1210,6 +1515,8 @@ export interface DashSqlTemplateSummary {
|
|
|
1210
1515
|
sites?: DashCodeSite[];
|
|
1211
1516
|
/** An unnamed statement its named version replaced (edited as it was named): that statement's id — not one to name. */
|
|
1212
1517
|
replacedBy?: string;
|
|
1518
|
+
/** Test SQL — written in a test, not in the services' code (apps/catalog sql-test.ts): not linted, named or reviewed by itself. */
|
|
1519
|
+
test?: DashSqlTestMark;
|
|
1213
1520
|
/** SELECT · INSERT · UPDATE · DELETE · …; a WITH reads as the statement it leads to. */
|
|
1214
1521
|
verb: string;
|
|
1215
1522
|
tables: string[];
|
|
@@ -1396,9 +1703,50 @@ export interface DashDbSchema {
|
|
|
1396
1703
|
rows: DashDbStatementStat[];
|
|
1397
1704
|
};
|
|
1398
1705
|
findings: DashDbFinding[];
|
|
1706
|
+
/** What the findings judged — and what a size floor left out, so «no finding» is not read as «nothing there». */
|
|
1707
|
+
checked?: DashDbChecked;
|
|
1399
1708
|
/** A read that failed, in Postgres's words. */
|
|
1400
1709
|
error?: string;
|
|
1401
1710
|
}
|
|
1711
|
+
/** One database's last read by LensMCP's own checks (`db.schema`), as `db://analysis` names it beside the analyzer's runs. */
|
|
1712
|
+
export interface DashDbChecksSeen {
|
|
1713
|
+
store: string;
|
|
1714
|
+
database: string;
|
|
1715
|
+
at: number;
|
|
1716
|
+
findings: number;
|
|
1717
|
+
byKind: Partial<Record<DashDbFinding['kind'], number>>;
|
|
1718
|
+
/** {@link DashDbChecked.note}: what the checks judged. */
|
|
1719
|
+
judged?: string;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* What LensMCP's own checks of a database judged (foodguard-83). The table checks (seq-scan, dead-rows,
|
|
1723
|
+
* unindexed-foreign-key) judge only tables of `floorRows` rows or more — on a development database that is often none.
|
|
1724
|
+
*/
|
|
1725
|
+
export interface DashDbChecked {
|
|
1726
|
+
tables: number;
|
|
1727
|
+
/** Tables at or over the floor: the ones the table checks judged. */
|
|
1728
|
+
judged: number;
|
|
1729
|
+
floorRows: number;
|
|
1730
|
+
/** Under the floor, the most sequentially scanned — not judged, named so the floor hides nothing. */
|
|
1731
|
+
belowFloor: Array<{
|
|
1732
|
+
table: string;
|
|
1733
|
+
rows: number;
|
|
1734
|
+
seqScans: number;
|
|
1735
|
+
}>;
|
|
1736
|
+
indexes: number;
|
|
1737
|
+
/**
|
|
1738
|
+
* Foreign keys no valid index leads with; of those, the ones the check judged (a big child, a parent with deletes),
|
|
1739
|
+
* and the ones under the floor whose referenced table has deletes — a cost once the child grows.
|
|
1740
|
+
*/
|
|
1741
|
+
foreignKeys?: {
|
|
1742
|
+
total: number;
|
|
1743
|
+
unindexed: number;
|
|
1744
|
+
judged: number;
|
|
1745
|
+
atScale: number;
|
|
1746
|
+
};
|
|
1747
|
+
/** The above in a sentence. */
|
|
1748
|
+
note: string;
|
|
1749
|
+
}
|
|
1402
1750
|
export interface DashDbTableInfo {
|
|
1403
1751
|
schema: string;
|
|
1404
1752
|
name: string;
|
|
@@ -1430,6 +1778,8 @@ export interface DashDbIndex {
|
|
|
1430
1778
|
valid: boolean;
|
|
1431
1779
|
/** Part of a partitioned table's index (or that index itself): changed on the parent, never on its own. */
|
|
1432
1780
|
partition?: boolean;
|
|
1781
|
+
/** The primary key, unique or exclusion constraint it backs — dropping it drops the constraint. */
|
|
1782
|
+
constraint?: string;
|
|
1433
1783
|
scans: number;
|
|
1434
1784
|
rowsRead: number;
|
|
1435
1785
|
bytes: number;
|
|
@@ -1665,7 +2015,7 @@ export interface DashDbStatementStat {
|
|
|
1665
2015
|
hitRatio?: number;
|
|
1666
2016
|
}
|
|
1667
2017
|
export interface DashDbFinding {
|
|
1668
|
-
kind: 'unused-index' | 'duplicate-index' | 'invalid-index' | 'seq-scan' | 'dead-rows';
|
|
2018
|
+
kind: 'unused-index' | 'duplicate-index' | 'invalid-index' | 'seq-scan' | 'dead-rows' | 'unindexed-foreign-key';
|
|
1669
2019
|
severity: 'high' | 'medium' | 'low';
|
|
1670
2020
|
table: string;
|
|
1671
2021
|
index?: string;
|
|
@@ -1673,6 +2023,25 @@ export interface DashDbFinding {
|
|
|
1673
2023
|
detail: string;
|
|
1674
2024
|
/** The statement that would fix it — shown to copy, never run by the lens. */
|
|
1675
2025
|
fix?: string;
|
|
2026
|
+
/** An index finding: the index's own scans and size. */
|
|
2027
|
+
scans?: number;
|
|
2028
|
+
bytes?: number;
|
|
2029
|
+
/** duplicate-index: the index that stays, and why it is enough (`backs <constraint>`, `unique`, `wider`, `the same`). */
|
|
2030
|
+
kept?: {
|
|
2031
|
+
index: string;
|
|
2032
|
+
why: string;
|
|
2033
|
+
scans: number;
|
|
2034
|
+
bytes: number;
|
|
2035
|
+
};
|
|
2036
|
+
/** The table's inserts + updates since Postgres began counting — each index costs a write on every one. */
|
|
2037
|
+
writes?: number;
|
|
2038
|
+
/** unindexed-foreign-key: the constraint, and the table it references. */
|
|
2039
|
+
foreignKey?: {
|
|
2040
|
+
name: string;
|
|
2041
|
+
columns: string[];
|
|
2042
|
+
references: string;
|
|
2043
|
+
onDelete: string;
|
|
2044
|
+
};
|
|
1676
2045
|
}
|
|
1677
2046
|
/** One Redis command by its shape: the command and its key with ids folded (`GET session:*`); a script by its hash. */
|
|
1678
2047
|
export interface DashKvCommandShape {
|
|
@@ -1926,12 +2295,18 @@ export interface DashAiAnalyzer {
|
|
|
1926
2295
|
folder?: string;
|
|
1927
2296
|
/** Its runs in this checkout. */
|
|
1928
2297
|
runs: number;
|
|
2298
|
+
/**
|
|
2299
|
+
* LensMCP's guide it reviews by. `/api/ai/analyzers` leaves its `text` out and says its length (`chars`):
|
|
2300
|
+
* `/api/ai/analyzers/guide?id=` ({@link DashAiAnalyzerGuide}) returns it when Settings shows it — six guides and
|
|
2301
|
+
* six previews in every list were 1 MB a read (FoodGuard, 2026-10-20).
|
|
2302
|
+
*/
|
|
1929
2303
|
guide?: {
|
|
1930
2304
|
id: string;
|
|
1931
2305
|
title: string;
|
|
1932
2306
|
version: number;
|
|
1933
2307
|
sha: string;
|
|
1934
|
-
text
|
|
2308
|
+
text?: string;
|
|
2309
|
+
chars?: number;
|
|
1935
2310
|
};
|
|
1936
2311
|
indicators: Array<{
|
|
1937
2312
|
id: string;
|
|
@@ -1954,10 +2329,14 @@ export interface DashAiAnalyzer {
|
|
|
1954
2329
|
path: string;
|
|
1955
2330
|
why: string;
|
|
1956
2331
|
}>;
|
|
1957
|
-
/**
|
|
2332
|
+
/**
|
|
2333
|
+
* The instructions every run of it sends now, before the evidence: their size in the list (`tokens`, `chars`); the
|
|
2334
|
+
* text itself (`system`) from `/api/ai/analyzers/guide?id=`, like the guide's.
|
|
2335
|
+
*/
|
|
1958
2336
|
preview: {
|
|
1959
|
-
system
|
|
2337
|
+
system?: string;
|
|
1960
2338
|
tokens: number;
|
|
2339
|
+
chars?: number;
|
|
1961
2340
|
};
|
|
1962
2341
|
lastRun?: {
|
|
1963
2342
|
at: number;
|
|
@@ -1978,6 +2357,21 @@ export interface DashAiAnalyzer {
|
|
|
1978
2357
|
model: string;
|
|
1979
2358
|
};
|
|
1980
2359
|
}
|
|
2360
|
+
/** `GET /api/ai/analyzers/guide?id=<analyzer>`: one analyzer's guide and what a run of it sends — fetched when Settings shows them. */
|
|
2361
|
+
export interface DashAiAnalyzerGuide {
|
|
2362
|
+
id: string;
|
|
2363
|
+
guide?: {
|
|
2364
|
+
id: string;
|
|
2365
|
+
title: string;
|
|
2366
|
+
version: number;
|
|
2367
|
+
sha: string;
|
|
2368
|
+
text: string;
|
|
2369
|
+
};
|
|
2370
|
+
preview: {
|
|
2371
|
+
system: string;
|
|
2372
|
+
tokens: number;
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
1981
2375
|
/** How hard a frontier model thinks (Claude 4.6 and later; other models are sent none). */
|
|
1982
2376
|
export type DashAiEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
1983
2377
|
export interface DashAiAnalyzers {
|
|
@@ -2045,8 +2439,13 @@ export interface DashAiRun {
|
|
|
2045
2439
|
analyzerName: string;
|
|
2046
2440
|
question: string;
|
|
2047
2441
|
/** What was reviewed; `name` is a statement's name (its leading comment) — the same across its versions. */
|
|
2442
|
+
/**
|
|
2443
|
+
* `db`: one database (`<store>/<database>`, the health check); `dbtable`: one table (`<store>/<database>/<schema>.<table>`);
|
|
2444
|
+
* `kvstore` / `queuestore`: one Redis store's keys / BullMQ queues (`<store>`); `files`: the services' file access
|
|
2445
|
+
* (`files`); `external`: the external APIs the services call (`external`); `log`: one log line (its `seq`).
|
|
2446
|
+
*/
|
|
2048
2447
|
target: {
|
|
2049
|
-
kind: 'sql' | 'route' | 'error' | 'flow' | 'ui' | 'kv' | 'queue';
|
|
2448
|
+
kind: 'sql' | 'route' | 'error' | 'flow' | 'ui' | 'kv' | 'queue' | 'db' | 'dbtable' | 'kvstore' | 'queuestore' | 'files' | 'external' | 'log' | 'page';
|
|
2050
2449
|
id: string;
|
|
2051
2450
|
label: string;
|
|
2052
2451
|
name?: string;
|
|
@@ -2144,7 +2543,13 @@ export interface DashAiRun {
|
|
|
2144
2543
|
instructions: string;
|
|
2145
2544
|
};
|
|
2146
2545
|
error?: string;
|
|
2546
|
+
/**
|
|
2547
|
+
* The run was never sent, so nothing was reviewed: no key could pay (`no-key`), the trigger's daily allowance was
|
|
2548
|
+
* spent (`over-daily-cap`), or every key was out of rate-limit room (`no-room`). A refusal, not a failed review.
|
|
2549
|
+
*/
|
|
2550
|
+
refused?: DashAiRefusal;
|
|
2147
2551
|
}
|
|
2552
|
+
export type DashAiRefusal = 'no-key' | 'over-daily-cap' | 'no-room';
|
|
2148
2553
|
/**
|
|
2149
2554
|
* An analysis running or waiting now, in the workspace's shared queue: one per `key` (the analyzer and its target),
|
|
2150
2555
|
* at most the workspace's limit running together, the rest waiting in the order they came.
|
|
@@ -2230,11 +2635,12 @@ export interface DashReviewStatus {
|
|
|
2230
2635
|
turns?: number;
|
|
2231
2636
|
tools?: number;
|
|
2232
2637
|
};
|
|
2233
|
-
/** The newest run, when it failed: when, and its error. */
|
|
2638
|
+
/** The newest run, when it failed: when, and its error — `refused` when it was never sent (DashAiRun.refused). */
|
|
2234
2639
|
failed?: {
|
|
2235
2640
|
runId: string;
|
|
2236
2641
|
at: number;
|
|
2237
2642
|
error: string;
|
|
2643
|
+
refused?: DashAiRefusal;
|
|
2238
2644
|
};
|
|
2239
2645
|
}
|
|
2240
2646
|
/** A finished review in a few lines: when, its score, what it is about, the changes it asks for first. */
|
package/lib/dashboard.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";export const DASHBOARD_PROTOCOL=1,DASH_PERF_KINDS=["n-plus-one","db-in-loop","render-storm","slow-path","slow-route","slow-query","missing-index","leak","egress-slow","duplicate-requests","perf-rule","db-connection","other"],DASH_AI_TRIGGERS=["auto","click","agent"];
|
|
1
|
+
"use strict";export const DASHBOARD_PROTOCOL=1,DASH_PERF_KINDS=["n-plus-one","db-in-loop","render-storm","slow-path","slow-route","slow-query","missing-index","leak","egress-slow","duplicate-requests","perf-rule","db-connection","other"],DASH_STREAM_TOPICS=["summary","session","cluster","kpis","flows","flow-rev","logs","diagnostics","errors"],DASH_AI_TRIGGERS=["auto","click","agent"];
|
package/lib/http-exchange.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One HTTP exchange the lens captured (R6): a service's outbound call
|
|
2
|
+
* One HTTP exchange the lens captured (R6): a service's outbound call (`egress`), or a request it served (`ingress`
|
|
3
|
+
* — a route's run, as it came in and went out), as it went over the wire — its request and
|
|
3
4
|
* response headers (redacted), the query string and the bodies (redacted, the first 64 KB of each), and for a
|
|
4
5
|
* server-sent-events response every event with the moment it arrived. Kept OFF the bus, one file per exchange under
|
|
5
6
|
* `.lensmcp/http/`; the bus event carries its `exchangeId`. Capture: `LENSMCP_HTTP_CAPTURE=off | headers | bodies`.
|
|
@@ -17,8 +18,11 @@ export interface HttpExchangeBody {
|
|
|
17
18
|
/** A binary body: its digest instead of its bytes (when it was small enough to hash). */
|
|
18
19
|
sha256?: string;
|
|
19
20
|
truncated: boolean;
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Why there is no text: `binary`, `form-data`, `stream` (a request body sent as a stream), `ai`, `encoded` (a
|
|
23
|
+
* compressed answer too big to unpack).
|
|
24
|
+
*/
|
|
25
|
+
omitted?: 'binary' | 'form-data' | 'stream' | 'ai' | 'encoded';
|
|
22
26
|
}
|
|
23
27
|
export interface HttpExchangeStreamEvent {
|
|
24
28
|
/** Since the request started. */
|
|
@@ -32,7 +36,7 @@ export interface HttpExchange {
|
|
|
32
36
|
schemaVersion: 1;
|
|
33
37
|
id: string;
|
|
34
38
|
capturedAt: number;
|
|
35
|
-
side: 'egress';
|
|
39
|
+
side: 'egress' | 'ingress';
|
|
36
40
|
service?: string;
|
|
37
41
|
flowId?: string;
|
|
38
42
|
requestId?: string;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page visit, as the browser measures it (planning/pages/00-plan.md §2): from navigation start (a hard load) or a
|
|
3
|
+
* pathname change (an SPA navigation) to "ready" — the first commit after the page's own requests settle — and on to
|
|
4
|
+
* its end (the next visit, `pagehide`, or a minute after ready). A visit is not a flow: it records the flows its work
|
|
5
|
+
* ran under. The tab sends one event when the visit is ready (`phase: 'ready'`) and one when it ends (`phase: 'end'`);
|
|
6
|
+
* the lens joins its requests to the backend's routes and SQL by request id and folds it into the page catalog.
|
|
7
|
+
*
|
|
8
|
+
* Field names stay clear of the redactor's secret words (key, code, session, token …): it would blank their values.
|
|
9
|
+
* No query string is ever sent — a request names its pathname and a hash of its query.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PAGE_VISIT_KIND: "page-visit";
|
|
12
|
+
/** How the visit began. A hard load: the navigation's type; an SPA navigation: the history call. */
|
|
13
|
+
export type PageVisitNavType = 'navigate' | 'reload' | 'back_forward' | 'push' | 'replace' | 'pop';
|
|
14
|
+
/**
|
|
15
|
+
* How ready was decided: `settled` — the first commit after the page's requests settled, with none started within the
|
|
16
|
+
* quiet window; `no-requests` — it made none (the first commit that held); `no-commit` — the requests settled and
|
|
17
|
+
* nothing committed within a second after (the last commit before); `timeout` — not ready within the cap (its open
|
|
18
|
+
* requests are named); `left` — the next visit began first.
|
|
19
|
+
*/
|
|
20
|
+
export type PageVisitReadyBy = 'settled' | 'no-requests' | 'no-commit' | 'timeout' | 'left';
|
|
21
|
+
/** One request the page made during the visit (every `fetch`, with or without a flow). */
|
|
22
|
+
export interface PageVisitRequest {
|
|
23
|
+
/** The request id the lens's fetch hook stamped (`x-request-id`): what the backend's server-request carries. */
|
|
24
|
+
rid: string;
|
|
25
|
+
/** Method. */
|
|
26
|
+
m: string;
|
|
27
|
+
/** Host + pathname (no query): `api.foodguard.localhost/registry/v1/business-profile/situation-state`. */
|
|
28
|
+
p: string;
|
|
29
|
+
/** A short hash of the query string, so two calls with different queries tell apart — never the query. */
|
|
30
|
+
qh?: string;
|
|
31
|
+
/** When it started, in ms after the visit's start. */
|
|
32
|
+
s: number;
|
|
33
|
+
/** Ms until its response headers (a streamed body does not count); absent while still open. */
|
|
34
|
+
d?: number;
|
|
35
|
+
/** HTTP status; 0 for a network failure. */
|
|
36
|
+
st?: number;
|
|
37
|
+
/** The flow the request ran under, if one was open. */
|
|
38
|
+
flowId?: string;
|
|
39
|
+
/** Started after ready (polling, a lazy panel). */
|
|
40
|
+
late?: true;
|
|
41
|
+
}
|
|
42
|
+
export interface PageVisitRenders {
|
|
43
|
+
commits: number;
|
|
44
|
+
/** Components rendered, summed over the commits. */
|
|
45
|
+
renders: number;
|
|
46
|
+
/** React's own render time (actualDuration), summed. */
|
|
47
|
+
renderMs: number;
|
|
48
|
+
maxCommitMs: number;
|
|
49
|
+
/** Commits that rendered more than the storm threshold. */
|
|
50
|
+
stormCommits: number;
|
|
51
|
+
mounts: number;
|
|
52
|
+
/** Commits that showed a Suspense fallback (a lazy chunk or data still loading). */
|
|
53
|
+
suspenseWaits: number;
|
|
54
|
+
/** The most-rendered components, by renders: name, renders, ms. At most 8. */
|
|
55
|
+
top: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
n: number;
|
|
58
|
+
ms: number;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
export interface PageVisitStore {
|
|
62
|
+
updates: number;
|
|
63
|
+
/** The most-updated store paths. At most 8. */
|
|
64
|
+
top: Array<{
|
|
65
|
+
store: string;
|
|
66
|
+
path: string;
|
|
67
|
+
n: number;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
/** The browser's own timings of a hard load (Web Vitals and friends) — on a Vite dev server, not a production build. */
|
|
71
|
+
export interface PageVisitVitals {
|
|
72
|
+
ttfb?: number;
|
|
73
|
+
fcp?: number;
|
|
74
|
+
lcp?: number;
|
|
75
|
+
cls?: number;
|
|
76
|
+
inp?: number;
|
|
77
|
+
dcl?: number;
|
|
78
|
+
load?: number;
|
|
79
|
+
}
|
|
80
|
+
export interface PageVisitRaw {
|
|
81
|
+
kind: typeof PAGE_VISIT_KIND;
|
|
82
|
+
phase: 'ready' | 'end';
|
|
83
|
+
visitId: string;
|
|
84
|
+
/** The browser tab (the client runtime's id), so a tab's visits read in order. */
|
|
85
|
+
tabId?: string;
|
|
86
|
+
/** The app the Vite plugin serves (its project name), or the page's host. */
|
|
87
|
+
app: string;
|
|
88
|
+
/** The router's full match pattern (`/companies/:id/onboarding/chat`) — absent when no router pattern is known. */
|
|
89
|
+
pattern?: string;
|
|
90
|
+
/** The pathname the visit opened (≤ 200 characters, no query). */
|
|
91
|
+
path: string;
|
|
92
|
+
nav: 'hard' | 'spa';
|
|
93
|
+
navType?: PageVisitNavType;
|
|
94
|
+
/** A hard load the dev server's hot reload caused. */
|
|
95
|
+
cause?: 'hmr';
|
|
96
|
+
/** The pathname a `replace` before ready came from (a redirect): the visit went on. */
|
|
97
|
+
redirectedFrom?: string;
|
|
98
|
+
/** Epoch ms: navigation start (hard) or the history call (SPA). */
|
|
99
|
+
startAt: number;
|
|
100
|
+
readyAt?: number;
|
|
101
|
+
readyBy?: PageVisitReadyBy;
|
|
102
|
+
/** Ms from start to ready. */
|
|
103
|
+
readyMs?: number;
|
|
104
|
+
/** A hard load: ms from the app's entry module to ready — what the app took, without the dev server serving it. */
|
|
105
|
+
appReadyMs?: number;
|
|
106
|
+
endAt?: number;
|
|
107
|
+
/** The flows the visit's work ran under. */
|
|
108
|
+
flowIds: string[];
|
|
109
|
+
/** Up to 60 requests; the rest counted. */
|
|
110
|
+
requests: PageVisitRequest[];
|
|
111
|
+
requestsDropped?: number;
|
|
112
|
+
renders: PageVisitRenders;
|
|
113
|
+
store: PageVisitStore;
|
|
114
|
+
errors: {
|
|
115
|
+
n: number;
|
|
116
|
+
first?: string;
|
|
117
|
+
};
|
|
118
|
+
/** From ready to the end (the `end` event only). */
|
|
119
|
+
afterReady?: {
|
|
120
|
+
requests: number;
|
|
121
|
+
templates: string[];
|
|
122
|
+
renders: number;
|
|
123
|
+
updates: number;
|
|
124
|
+
};
|
|
125
|
+
vitals?: PageVisitVitals;
|
|
126
|
+
longTasks?: {
|
|
127
|
+
n: number;
|
|
128
|
+
totalMs: number;
|
|
129
|
+
maxMs: number;
|
|
130
|
+
};
|
|
131
|
+
heapMb?: number;
|
|
132
|
+
chunks?: {
|
|
133
|
+
n: number;
|
|
134
|
+
kb: number;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";export const PAGE_VISIT_KIND="page-visit";
|
package/lib/sources.d.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CUSTOM SOURCES (issues/open/foodguard-81) — an application REPORTS what the lens cannot observe, as one of a closed
|
|
3
|
+
* set of typed kinds the lens knows how to draw, threshold and explain to an agent: `queue` · `loop` · `store` ·
|
|
4
|
+
* `traffic`. Phase 1 reads `queue` only — work a service keeps waiting in its OWN storage (an outbox table), which no
|
|
5
|
+
* event stream can show: FoodGuard's lens showed its BullMQ queues nearly empty while 6,389 outbox rows waited.
|
|
6
|
+
*
|
|
7
|
+
* Three hops, each with its own shape:
|
|
8
|
+
*
|
|
9
|
+
* 1. The WIRE — the snapshot a service answers on the dev route it declares in project.json (`cluster.sources`):
|
|
10
|
+
* `{ schema: 'lensmcp.queue/v1', at, truncated?: { groups }, groups: [{ key, labels?, pending, inFlight?, failed?,
|
|
11
|
+
* oldestPendingAt?, completedInWindow? }] }`. The service owns its query; the lens never connects to an app's
|
|
12
|
+
* database. `@lensmcp/cluster` parses it (`source-poller.ts`) — a snapshot that does not parse is `malformed`, never
|
|
13
|
+
* zero.
|
|
14
|
+
* 2. The BUS — the gateway's poller reports each poll as a `source: 'gateway'`, `category: 'cluster'` event whose
|
|
15
|
+
* `raw.kind` is `source-poll` ({@link SourcePollRaw}), and each service's declaration as `source-declaration`
|
|
16
|
+
* ({@link SourceDeclarationRaw}). A group's `key` travels as `group`: the bus redacts any field NAMED `key`
|
|
17
|
+
* (`@lensmcp/core` `isSecretParam`), which would have turned every group into `[REDACTED]`. The cluster MIRRORS
|
|
18
|
+
* these shapes (it does not depend on this package); a parity fixture pins both sides.
|
|
19
|
+
* 3. The READ — `apps/agent` folds them into `process://sources` ({@link SourcesReport}); the `observe.sources` and
|
|
20
|
+
* `source.get` tools, the status signal and the dashboard read that.
|
|
21
|
+
*
|
|
22
|
+
* Everything here is REPORTED by the app, never measured by the lens: every read carries `provenance: 'reported'`.
|
|
23
|
+
* Additive only.
|
|
24
|
+
*/
|
|
25
|
+
/** The closed set of source kinds. */
|
|
26
|
+
export declare const SOURCE_KINDS: readonly ["queue", "loop", "store", "traffic"];
|
|
27
|
+
export type SourceKind = (typeof SOURCE_KINDS)[number];
|
|
28
|
+
/** The kinds this release reads; a declared kind outside it is said, not polled. */
|
|
29
|
+
export declare const SOURCE_KINDS_READ: readonly ["queue"];
|
|
30
|
+
export type SourceKindRead = (typeof SOURCE_KINDS_READ)[number];
|
|
31
|
+
/** Every snapshot's schema: `lensmcp.<kind>/v1`. */
|
|
32
|
+
export declare const sourceWireSchema: (kind: SourceKind) => string;
|
|
33
|
+
export declare const QUEUE_WIRE_SCHEMA = "lensmcp.queue/v1";
|
|
34
|
+
/** The resource the lens folds the polls into. */
|
|
35
|
+
export declare const URI_SOURCES = "process://sources";
|
|
36
|
+
/** Said on every read of a source: the app's claim, not the lens's measurement. */
|
|
37
|
+
export declare const SOURCE_PROVENANCE: "reported";
|
|
38
|
+
/** The finding's rule id: a queue group whose oldest pending item has waited longer than `thresholds.oldestWaitMin`. */
|
|
39
|
+
export declare const QUEUE_OLDEST_WAIT_RULE = "queue/oldest-wait";
|
|
40
|
+
export declare const SOURCE_EVENT_KINDS: readonly ["source-poll", "source-declaration"];
|
|
41
|
+
export type SourceEventKind = (typeof SOURCE_EVENT_KINDS)[number];
|
|
42
|
+
/** What ONE poll found. */
|
|
43
|
+
export declare const SOURCE_POLL_STATES: readonly ["current", "unreachable", "refused", "malformed", "not-running"];
|
|
44
|
+
export type SourcePollState = (typeof SOURCE_POLL_STATES)[number];
|
|
45
|
+
/**
|
|
46
|
+
* What the lens says of a source when it is READ: a poll's state; `stale` — its newest report is overdue (the gateway
|
|
47
|
+
* stopped polling, or its polls are held up), so whatever it last said is no longer known; or `never-polled` —
|
|
48
|
+
* declared, and no poll has reported yet.
|
|
49
|
+
*/
|
|
50
|
+
export declare const SOURCE_STATES: readonly ["current", "unreachable", "refused", "malformed", "not-running", "stale", "never-polled"];
|
|
51
|
+
export type SourceState = (typeof SOURCE_STATES)[number];
|
|
52
|
+
export declare const SOURCE_DEFAULT_INTERVAL_SEC = 10;
|
|
53
|
+
export declare const SOURCE_MIN_INTERVAL_SEC = 2;
|
|
54
|
+
export declare const QUEUE_DEFAULT_OLDEST_WAIT_MIN = 10;
|
|
55
|
+
/** A snapshot holds at most this many groups; a producer with more caps it and says `truncated: { groups }`. */
|
|
56
|
+
export declare const SOURCE_MAX_GROUPS = 500;
|
|
57
|
+
export declare const SOURCE_MAX_BYTES: number;
|
|
58
|
+
/** A report is overdue — `stale` — once it is this many report periods old (and never sooner than 30 s). */
|
|
59
|
+
export declare const SOURCE_STALE_PERIODS = 3;
|
|
60
|
+
export declare const SOURCE_STALE_MIN_MS = 30000;
|
|
61
|
+
/** When a source's newest report stops being trusted. */
|
|
62
|
+
export declare function sourceStaleAfterMs(reportEveryMs: number): number;
|
|
63
|
+
/** A queue source's thresholds. `stalledMin` is accepted now and judged once the lens keeps a history (phase 2). */
|
|
64
|
+
export interface QueueThresholds {
|
|
65
|
+
oldestWaitMin: number;
|
|
66
|
+
stalledMin?: number;
|
|
67
|
+
}
|
|
68
|
+
/** One declared source, as the gateway polls it. */
|
|
69
|
+
export interface SourceDeclRaw {
|
|
70
|
+
sourceKind: SourceKindRead;
|
|
71
|
+
name: string;
|
|
72
|
+
path: string;
|
|
73
|
+
intervalSec: number;
|
|
74
|
+
thresholds: QueueThresholds;
|
|
75
|
+
}
|
|
76
|
+
/** One group of a queue snapshot, as the bus carries it. */
|
|
77
|
+
export interface QueueGroupRaw {
|
|
78
|
+
/** The service's own key for the group (`tenant/kind`) — the wire's `key`. */
|
|
79
|
+
group: string;
|
|
80
|
+
labels?: Record<string, string>;
|
|
81
|
+
pending: number;
|
|
82
|
+
inFlight?: number;
|
|
83
|
+
failed?: number;
|
|
84
|
+
/** When the oldest pending item of the group was written (ISO). `null`/absent: the service did not say. */
|
|
85
|
+
oldestPendingAt?: string | null;
|
|
86
|
+
completedInWindow?: number;
|
|
87
|
+
}
|
|
88
|
+
export interface QueueTotals {
|
|
89
|
+
groups: number;
|
|
90
|
+
pending: number;
|
|
91
|
+
inFlight: number;
|
|
92
|
+
failed: number;
|
|
93
|
+
/** Present when any group said it. */
|
|
94
|
+
completedInWindow?: number;
|
|
95
|
+
}
|
|
96
|
+
/** A queue snapshot that parsed. */
|
|
97
|
+
export interface QueueSnapshotRaw {
|
|
98
|
+
/** When the SERVICE measured it (its `at`, ISO) — a group's wait is measured at this time. */
|
|
99
|
+
at: string;
|
|
100
|
+
/** When the gateway received it (epoch ms) — the snapshot's age is measured from this. */
|
|
101
|
+
receivedAt: number;
|
|
102
|
+
totals: QueueTotals;
|
|
103
|
+
/** Every group the service answered (at most {@link SOURCE_MAX_GROUPS}), the oldest-waiting first. */
|
|
104
|
+
groups: QueueGroupRaw[];
|
|
105
|
+
/** The service capped its answer: this many groups were dropped, and are in no total. */
|
|
106
|
+
truncated?: {
|
|
107
|
+
groups: number;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** One poll of one declared source (`raw.kind: 'source-poll'`). */
|
|
111
|
+
export interface SourcePollRaw extends SourceDeclRaw {
|
|
112
|
+
kind: 'source-poll';
|
|
113
|
+
provenance: typeof SOURCE_PROVENANCE;
|
|
114
|
+
/** The Nx project that declares it. */
|
|
115
|
+
project: string;
|
|
116
|
+
state: SourcePollState;
|
|
117
|
+
/** Why, for every state but `current`: the error, the status line, what did not parse, why the service was not asked. */
|
|
118
|
+
detail?: string;
|
|
119
|
+
/** The HTTP status of a `refused` answer. */
|
|
120
|
+
status?: number;
|
|
121
|
+
polledAt: number;
|
|
122
|
+
durationMs?: number;
|
|
123
|
+
/** The gateway reports this source again within this many ms — what decides when the lens calls it `stale`. */
|
|
124
|
+
reportEveryMs: number;
|
|
125
|
+
/** Polls in a row that failed (`unreachable`/`refused`/`malformed`) — the gateway backs off while it grows. */
|
|
126
|
+
failures?: number;
|
|
127
|
+
/** The LAST GOOD snapshot: this poll's when `current`, else the one before (kept, with its age), if any. */
|
|
128
|
+
snapshot?: QueueSnapshotRaw;
|
|
129
|
+
/** The owning workspace (stamped by the gateway on every service event). */
|
|
130
|
+
wsKey?: string;
|
|
131
|
+
floodSuppressed?: number;
|
|
132
|
+
}
|
|
133
|
+
/** A service's `cluster.sources`, as the gateway read it (`raw.kind: 'source-declaration'`). */
|
|
134
|
+
export interface SourceDeclarationRaw {
|
|
135
|
+
kind: 'source-declaration';
|
|
136
|
+
project: string;
|
|
137
|
+
/** The entries it polls. */
|
|
138
|
+
sources: SourceDeclRaw[];
|
|
139
|
+
/** Each entry it does NOT poll, and why — an unknown kind or a malformed entry is said, never silently dropped. */
|
|
140
|
+
problems: string[];
|
|
141
|
+
wsKey?: string;
|
|
142
|
+
}
|
|
143
|
+
/** A `source-poll` event's `raw`, checked just enough to be read safely (telemetry is only partly trusted). */
|
|
144
|
+
export declare function asSourcePollRaw(raw: unknown): SourcePollRaw | undefined;
|
|
145
|
+
/** A `source-declaration` event's `raw`, checked just enough to be read safely. */
|
|
146
|
+
export declare function asSourceDeclarationRaw(raw: unknown): SourceDeclarationRaw | undefined;
|
|
147
|
+
/** One queue group, as the lens shows it. */
|
|
148
|
+
export interface QueueGroupView extends QueueGroupRaw {
|
|
149
|
+
/** How long its oldest pending item had waited when the service measured it (`at − oldestPendingAt`). */
|
|
150
|
+
oldestWaitMs?: number;
|
|
151
|
+
/** That wait is longer than the source's `thresholds.oldestWaitMin`. */
|
|
152
|
+
overThreshold: boolean;
|
|
153
|
+
}
|
|
154
|
+
/** A queue source's headline numbers. */
|
|
155
|
+
export interface QueueHeadline extends QueueTotals {
|
|
156
|
+
/** The longest wait among its groups, as of the snapshot. */
|
|
157
|
+
oldestWaitMs?: number;
|
|
158
|
+
/** Groups whose oldest wait is over the threshold. */
|
|
159
|
+
overThreshold: number;
|
|
160
|
+
}
|
|
161
|
+
/** One declared source, as the lens shows it. */
|
|
162
|
+
export interface SourceView extends SourceDeclRaw {
|
|
163
|
+
provenance: typeof SOURCE_PROVENANCE;
|
|
164
|
+
project: string;
|
|
165
|
+
state: SourceState;
|
|
166
|
+
/** Why, for every state but `current`. */
|
|
167
|
+
detail?: string;
|
|
168
|
+
status?: number;
|
|
169
|
+
/** When the gateway last polled it (absent: `never-polled`). */
|
|
170
|
+
polledAt?: number;
|
|
171
|
+
failures?: number;
|
|
172
|
+
/** The last good snapshot's age — present whenever one is held, whatever the state. */
|
|
173
|
+
ageMs?: number;
|
|
174
|
+
/** When the service measured the last good snapshot (its `at`). */
|
|
175
|
+
snapshotAt?: string;
|
|
176
|
+
headline?: QueueHeadline;
|
|
177
|
+
/** The oldest-waiting first. Empty with `state: 'current'` is a real «nothing pending». */
|
|
178
|
+
groups: QueueGroupView[];
|
|
179
|
+
truncated?: {
|
|
180
|
+
groups: number;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** A queue group whose oldest pending item has waited longer than its source allows. */
|
|
184
|
+
export interface SourceFinding {
|
|
185
|
+
rule: typeof QUEUE_OLDEST_WAIT_RULE;
|
|
186
|
+
provenance: typeof SOURCE_PROVENANCE;
|
|
187
|
+
project: string;
|
|
188
|
+
name: string;
|
|
189
|
+
sourceKind: SourceKindRead;
|
|
190
|
+
group: string;
|
|
191
|
+
labels?: Record<string, string>;
|
|
192
|
+
pending: number;
|
|
193
|
+
oldestWaitMs: number;
|
|
194
|
+
thresholdMin: number;
|
|
195
|
+
title: string;
|
|
196
|
+
fix: string[];
|
|
197
|
+
verify: string;
|
|
198
|
+
}
|
|
199
|
+
/** `process://sources`. */
|
|
200
|
+
export interface SourcesReport {
|
|
201
|
+
schemaVersion?: string;
|
|
202
|
+
revision?: number;
|
|
203
|
+
updatedAt?: string;
|
|
204
|
+
provenance: typeof SOURCE_PROVENANCE;
|
|
205
|
+
/** `none` until a service declares a source: nothing reports what the taps cannot see — not «nothing waits». */
|
|
206
|
+
producer: 'connected' | 'none';
|
|
207
|
+
detail?: string;
|
|
208
|
+
/** The clock every age here was measured against. */
|
|
209
|
+
readAt: number;
|
|
210
|
+
sources: SourceView[];
|
|
211
|
+
/** Groups over their threshold, from `current` snapshots — the longest wait first. */
|
|
212
|
+
findings: SourceFinding[];
|
|
213
|
+
/** Declaration entries the gateway does not poll, and why. */
|
|
214
|
+
problems: Array<{
|
|
215
|
+
project: string;
|
|
216
|
+
problem: string;
|
|
217
|
+
}>;
|
|
218
|
+
counts: Record<SourceState, number>;
|
|
219
|
+
/** Over the `current` queue snapshots only. */
|
|
220
|
+
totals: {
|
|
221
|
+
pending: number;
|
|
222
|
+
inFlight: number;
|
|
223
|
+
failed: number;
|
|
224
|
+
};
|
|
225
|
+
}
|
package/lib/sources.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var E=Object.defineProperty;var n=(r,t)=>E(r,"name",{value:t,configurable:!0});var f=Object.defineProperty,e=n((r,t)=>f(r,"name",{value:t,configurable:!0}),"n");export const SOURCE_KINDS=["queue","loop","store","traffic"],SOURCE_KINDS_READ=["queue"],sourceWireSchema=e(r=>`lensmcp.${r}/v1`,"sourceWireSchema"),QUEUE_WIRE_SCHEMA="lensmcp.queue/v1",URI_SOURCES="process://sources",SOURCE_PROVENANCE="reported",QUEUE_OLDEST_WAIT_RULE="queue/oldest-wait",SOURCE_EVENT_KINDS=["source-poll","source-declaration"],SOURCE_POLL_STATES=["current","unreachable","refused","malformed","not-running"],SOURCE_STATES=[...SOURCE_POLL_STATES,"stale","never-polled"],SOURCE_DEFAULT_INTERVAL_SEC=10,SOURCE_MIN_INTERVAL_SEC=2,QUEUE_DEFAULT_OLDEST_WAIT_MIN=10,SOURCE_MAX_GROUPS=500,SOURCE_MAX_BYTES=256*1024,SOURCE_STALE_PERIODS=3,SOURCE_STALE_MIN_MS=3e4;export function sourceStaleAfterMs(r){return Math.max(SOURCE_STALE_MIN_MS,SOURCE_STALE_PERIODS*Math.max(0,r))}n(sourceStaleAfterMs,"sourceStaleAfterMs"),e(sourceStaleAfterMs,"sourceStaleAfterMs");const o=e(r=>typeof r=="object"&&r!==null&&!Array.isArray(r),"isObj"),i=e(r=>typeof r=="number"&&Number.isInteger(r)&&r>=0,"isCount"),c=e(r=>typeof r=="number"&&Number.isFinite(r)&&r>0,"isTime"),a=e((r,t)=>typeof r=="string"&&r.length>0&&r.length<=t,"isStr");function l(r){if(!SOURCE_KINDS_READ.includes(r.sourceKind)||!a(r.name,200)||typeof r.path!="string"||typeof r.intervalSec!="number")return!1;const t=r.thresholds;return o(t)&&typeof t.oldestWaitMin=="number"&&t.oldestWaitMin>0}n(l,"f"),e(l,"asDecl");function p(r){if(!o(r)||!a(r.group,1e3)||!i(r.pending))return!1;for(const u of["inFlight","failed","completedInWindow"])if(r[u]!==void 0&&!i(r[u]))return!1;const t=r.oldestPendingAt;if(t!=null&&typeof t!="string")return!1;const s=r.labels;return s===void 0||o(s)&&Object.values(s).every(u=>typeof u=="string")}n(p,"l"),e(p,"asGroup");function S(r){if(!o(r)||typeof r.at!="string"||!c(r.receivedAt)||!Array.isArray(r.groups))return!1;const t=r.totals;if(!o(t)||!i(t.groups)||!i(t.pending)||!i(t.inFlight)||!i(t.failed))return!1;const s=r.truncated;return s!==void 0&&!(o(s)&&i(s.groups))?!1:r.groups.length<=SOURCE_MAX_GROUPS&&r.groups.every(p)}n(S,"d"),e(S,"asSnapshot");export function asSourcePollRaw(r){if(!(!o(r)||r.kind!=="source-poll"||!a(r.project,200)||!l(r))&&SOURCE_POLL_STATES.includes(r.state)&&!(!c(r.polledAt)||!c(r.reportEveryMs))&&!(r.snapshot!==void 0&&!S(r.snapshot))&&!(r.state==="current"&&r.snapshot===void 0))return r}n(asSourcePollRaw,"asSourcePollRaw"),e(asSourcePollRaw,"asSourcePollRaw");export function asSourceDeclarationRaw(r){if(!(!o(r)||r.kind!=="source-declaration"||!a(r.project,200))&&!(!Array.isArray(r.sources)||!r.sources.every(t=>o(t)&&l(t)))&&!(!Array.isArray(r.problems)||!r.problems.every(t=>typeof t=="string")))return r}n(asSourceDeclarationRaw,"asSourceDeclarationRaw"),e(asSourceDeclarationRaw,"asSourceDeclarationRaw");
|