@lensmcp/core 1.23.3 → 1.23.6
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 +3 -0
- package/index.js +1 -1
- package/lib/ai-content.d.ts +14 -0
- package/lib/ai-content.js +1 -0
- package/lib/flow-categories.d.ts +107 -0
- package/lib/flow-categories.js +1 -0
- package/lib/flow-segments.d.ts +126 -0
- package/lib/flow-segments.js +1 -0
- package/lib/flow-shape.js +1 -1
- package/lib/flows-config.d.ts +45 -0
- package/lib/flows-config.js +1 -0
- package/package.json +7 -2
package/index.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export * from './lib/fingerprint.js';
|
|
|
8
8
|
export * from './lib/reducer-registry.js';
|
|
9
9
|
export * from './lib/story.js';
|
|
10
10
|
export * from './lib/flow-shape.js';
|
|
11
|
+
export * from './lib/flow-segments.js';
|
|
12
|
+
export * from './lib/flow-categories.js';
|
|
13
|
+
export * from './lib/flows-config.js';
|
|
11
14
|
export * from './lib/patterns.js';
|
|
12
15
|
export * from './lib/thresholds.js';
|
|
13
16
|
export * from './lib/graph-from-events.js';
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";export*from"./lib/common.js";export*from"./lib/ulid.js";export*from"./lib/event-bus.js";export*from"./lib/event-redactor.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/flow-shape.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";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/event-redactor.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/flow-shape.js";export*from"./lib/flow-segments.js";export*from"./lib/flow-categories.js";export*from"./lib/flows-config.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";export*from"./lib/graph-from-events.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AiCallContent } from '@lensmcp/protocol-types';
|
|
2
|
+
export type { AiCallContent, AiMessageContent, AiToolCallContent, AiUsageContent, } from '@lensmcp/protocol-types';
|
|
3
|
+
/** Directory beside the bus file that holds one `<callId>.json` per captured call. */
|
|
4
|
+
export declare const AI_CONTENT_DIR = "ai";
|
|
5
|
+
export declare const isAiCallId: (value: unknown) => value is string;
|
|
6
|
+
/** `<dir of the bus file>/ai` — the same rendezvous every other `.lensmcp/` artefact uses. */
|
|
7
|
+
export declare function aiContentDir(busFile: string): string;
|
|
8
|
+
/** Absolute path of one call's file. Throws on anything that is not a well-formed call id. */
|
|
9
|
+
export declare function aiContentFile(busFile: string, id: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Read one captured call. Returns `undefined` for a malformed id, a missing file (never captured, or
|
|
12
|
+
* rotated away), or unreadable JSON — a reader must treat all three as "not available", not as a fault.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readAiContent(busFile: string, id: string): AiCallContent | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var a=Object.defineProperty;var r=(t,i)=>a(t,"name",{value:i,configurable:!0});var s=Object.defineProperty,n=r((t,i)=>s(t,"name",{value:i,configurable:!0}),"r");import*as c from"node:fs";import{dirname as l,join as o}from"node:path";export const AI_CONTENT_DIR="ai";const d=/^ai_[0-9a-z]{8,40}$/;export const isAiCallId=n(t=>typeof t=="string"&&d.test(t),"isAiCallId");export function aiContentDir(t){return o(l(t),AI_CONTENT_DIR)}r(aiContentDir,"aiContentDir"),n(aiContentDir,"aiContentDir");export function aiContentFile(t,i){if(!isAiCallId(i))throw new Error("invalid ai call id");return o(aiContentDir(t),`${i}.json`)}r(aiContentFile,"aiContentFile"),n(aiContentFile,"aiContentFile");export function readAiContent(t,i){if(isAiCallId(i))try{const e=JSON.parse(c.readFileSync(aiContentFile(t,i),"utf8"));return e&&e.schemaVersion===1&&e.id===i?e:void 0}catch{return}}r(readAiContent,"readAiContent"),n(readAiContent,"readAiContent");
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow categories — what KIND of thing is this flow, so a list of 80 can be read as five groups.
|
|
3
|
+
*
|
|
4
|
+
* Two layers, in this order:
|
|
5
|
+
*
|
|
6
|
+
* 1. YOUR rules (`CategoryRule[]`, from `nx.json#lensmcp.flows.categories`). A flow is "checkout" or
|
|
7
|
+
* "login" because of your routes and your components — nothing built in can know that. First rule
|
|
8
|
+
* that matches wins, so order them most specific first.
|
|
9
|
+
* 2. The built-in classification, from facts every flow already carries: how it started (a page
|
|
10
|
+
* load, a click, an AI turn…), whether it is a test script, and — when nothing started it, like a
|
|
11
|
+
* `curl` — the shape of what happened.
|
|
12
|
+
*
|
|
13
|
+
* It also derives TAGS: cheap, orthogonal facts a person filters on ("show me flows with an error that
|
|
14
|
+
* fan out to 10+ requests"). Tags describe; the category classifies. A flow has one category and any
|
|
15
|
+
* number of tags.
|
|
16
|
+
*
|
|
17
|
+
* Pure. Takes the flow's events and its `segmentFlow` result; never reads anything else.
|
|
18
|
+
*/
|
|
19
|
+
import type { BaseEvent } from '@lensmcp/protocol-types';
|
|
20
|
+
import type { FlowSegment, FlowSegmentation } from './flow-segments.js';
|
|
21
|
+
export type BuiltinCategory = 'page-load' | 'user-action' | 'navigation' | 'api' | 'background' | 'ai' | 'system' | 'test' | 'other';
|
|
22
|
+
/** The order groups are shown in: what a person is most likely looking for first. */
|
|
23
|
+
export declare const CATEGORY_ORDER: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* A user-defined category. Every field you give must match (globs: `*` any run, `?` one character);
|
|
26
|
+
* a rule with NO match fields matches nothing — an empty rule must not swallow every flow.
|
|
27
|
+
*/
|
|
28
|
+
export interface CategoryRule {
|
|
29
|
+
category: string;
|
|
30
|
+
/** Glob on the flow id. */
|
|
31
|
+
flowId?: string;
|
|
32
|
+
/** Exact origin type: `page-load`, `user-click`, `form-submit`, `ai-turn`… */
|
|
33
|
+
originType?: string;
|
|
34
|
+
/** Glob on the origin node id — usually `ui:<file>:<line>:<handler>` — i.e. WHICH component started it. */
|
|
35
|
+
origin?: string;
|
|
36
|
+
/** Glob on `METHOD /path` (or just `/path`) of any request in the flow. */
|
|
37
|
+
route?: string;
|
|
38
|
+
/** Glob on the name of any service the flow touched. */
|
|
39
|
+
project?: string;
|
|
40
|
+
}
|
|
41
|
+
/** How a flow started: the origin type (`page-load`, `user-click`…) and the node that started it. */
|
|
42
|
+
export interface FlowOrigin {
|
|
43
|
+
type?: string;
|
|
44
|
+
nodeId?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface FlowClassification {
|
|
47
|
+
category: string;
|
|
48
|
+
/** Why: which layer decided. */
|
|
49
|
+
decidedBy: 'rule' | 'test' | 'origin' | 'shape';
|
|
50
|
+
/** Index into the rules array when `decidedBy` is `rule`. */
|
|
51
|
+
ruleIndex?: number;
|
|
52
|
+
tags: string[];
|
|
53
|
+
origin: FlowOrigin;
|
|
54
|
+
/**
|
|
55
|
+
* What the flow is ABOUT, below its category: the page for a page load, the component for a click,
|
|
56
|
+
* the root route for an api call, the script for a test. "page-load ×74" says nothing; "page-load ›
|
|
57
|
+
* /companies/:id/products ×13" is a place you can go and look.
|
|
58
|
+
*/
|
|
59
|
+
subject?: string;
|
|
60
|
+
}
|
|
61
|
+
/** `*` matches any run of characters, `?` one; everything else is literal. Case-sensitive. */
|
|
62
|
+
export declare function globMatch(pattern: string, text: string): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* `/companies/3f2a…/products/118` → `/companies/:id/products/:id`. Ids are what make every page load
|
|
65
|
+
* look unique; the template is what makes ten of them the same page. A segment is an id when it is a
|
|
66
|
+
* UUID, a long hex string, or a number of two or more digits — a single digit is more likely a version
|
|
67
|
+
* or an index than an identity.
|
|
68
|
+
*/
|
|
69
|
+
export declare function templatePath(path: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* `e2e-boom-muiw2v3pccj` → `e2e-boom`; `verify-E2F7E78F-C940-4726-…` → `verify`; `e2e-checkout-170000` →
|
|
72
|
+
* `e2e-checkout`. A script names a family and stamps each RUN with an id; the id is what to drop. A
|
|
73
|
+
* token counts as an id only if it is long enough AND contains a digit, so `e2e-checkout-flow` keeps
|
|
74
|
+
* its words — "checkout" is eight letters, but it is not a run id.
|
|
75
|
+
*/
|
|
76
|
+
export declare function scriptFamily(flowId: string): string;
|
|
77
|
+
export declare function classifyFlow(events: readonly BaseEvent[], segmentation: FlowSegmentation, rules?: readonly CategoryRule[]): FlowClassification;
|
|
78
|
+
/** The category of ONE segment: your route/project rules first, else what kind of unit it is. */
|
|
79
|
+
export declare function categoryOfSegment(segment: FlowSegment, flow: FlowClassification, rules?: readonly CategoryRule[]): string;
|
|
80
|
+
/** One subject within a category, and the flows that share it. */
|
|
81
|
+
export interface SubjectGroup<T> {
|
|
82
|
+
subject: string | undefined;
|
|
83
|
+
count: number;
|
|
84
|
+
items: T[];
|
|
85
|
+
}
|
|
86
|
+
/** One category and the flows in it. */
|
|
87
|
+
export interface CategoryBucket<T> {
|
|
88
|
+
category: string;
|
|
89
|
+
count: number;
|
|
90
|
+
items: T[];
|
|
91
|
+
}
|
|
92
|
+
export interface CategoryGroup<T> {
|
|
93
|
+
category: string;
|
|
94
|
+
count: number;
|
|
95
|
+
/** The same flows, split by subject: biggest first, then alphabetically. Flows with no subject come last. */
|
|
96
|
+
subjects: Array<SubjectGroup<T>>;
|
|
97
|
+
items: T[];
|
|
98
|
+
}
|
|
99
|
+
/** Category, then subject, then flows — the shape a flow list is read in. */
|
|
100
|
+
export declare function groupFlows<T extends {
|
|
101
|
+
category: string;
|
|
102
|
+
subject?: string;
|
|
103
|
+
}>(items: readonly T[]): Array<CategoryGroup<T>>;
|
|
104
|
+
/** Group classified flows for a list: built-in categories in a fixed order, then yours alphabetically. */
|
|
105
|
+
export declare function groupByCategory<T extends {
|
|
106
|
+
category: string;
|
|
107
|
+
}>(items: readonly T[]): Array<CategoryBucket<T>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var q=Object.defineProperty;var c=(e,t)=>q(e,"name",{value:t,configurable:!0});var M=Object.defineProperty,s=c((e,t)=>M(e,"name",{value:t,configurable:!0}),"s");export const CATEGORY_ORDER=["user-action","page-load","navigation","api","ai","background","test","system","other"];const p=new Map;export function globMatch(e,t){let r=p.get(e);return r||(r=new RegExp(`^${e.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")}$`),p.size>500&&p.clear(),p.set(e,r)),r.test(t)}c(globMatch,"globMatch"),s(globMatch,"globMatch");const T=s(e=>/^[A-Z]+ /.test(e),"hasMethod");function w(e,t){return t.some(r=>r.route?T(e)?globMatch(e,`${r.method??"GET"} ${r.route}`):globMatch(e,r.route):!1)}c(w,"T"),s(w,"routeMatches");function m(e,t){return!([e.flowId,e.originType,e.origin,e.route,e.project].filter(r=>r!==void 0).length===0||e.flowId!==void 0&&!globMatch(e.flowId,t.flowId)||e.originType!==void 0&&e.originType!==t.origin.type||e.origin!==void 0&&!(t.origin.nodeId!==void 0&&globMatch(e.origin,t.origin.nodeId))||e.route!==void 0&&!w(e.route,t.segments)||e.project!==void 0&&!t.segments.some(r=>r.projects.some(n=>globMatch(e.project,n))))}c(m,"b"),s(m,"ruleMatches");const y=s(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isObj"),l=s(e=>typeof e=="string"&&e!==""?e:void 0,"str"),f=s(e=>y(e.raw)?l(e.raw.kind)??"":"","kindOf");function k(e){const t=[...e].sort((a,g)=>(Number(a.timestamp)||0)-(Number(g.timestamp)||0)||(a.id<g.id?-1:1)),r=t.find(a=>f(a)==="flow-origin"),n=r??t.find(a=>l(a.context?.originType));if(!n)return{};const o=y(n.raw)?n.raw:{},i=l(n.context?.originType)??(r?l(n.title):void 0),u=l(o.originNodeId)??l(n.context?.originNodeId);return{...i?{type:i}:{},...u?{nodeId:u}:{}}}c(k,"$"),s(k,"originOf");const F=/^(verify|e2e|test|smoke|load)[-_:]/i,S=s((e,t)=>F.test(e)||t!==void 0&&/^(ui:)?(tools|scripts?|e2e)\//.test(t),"isTestFlow"),I={"page-load":"page-load","user-click":"user-action","form-submit":"user-action","input-change":"user-action","double-click":"user-action","context-menu":"user-action","key-press":"user-action","route-load":"navigation","route-change":"navigation",navigation:"navigation","ai-turn":"ai",timer:"background",cron:"background",interval:"background",queue:"background",job:"background","websocket-message":"background","hmr-update":"system"},z=new Set(["page-load","user-action","navigation"]),A=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;export function templatePath(e){return e.replace(/[?#].*$/,"").split("/").map(t=>A.test(t)||/^[0-9a-f]{16,}$/i.test(t)||/^\d{2,}$/.test(t)?":id":t).join("/")}c(templatePath,"templatePath"),s(templatePath,"templatePath");function $(e){const t=/^ui:(?:.*\/)?([^/:]+?)(?:\.[a-z]+)?:\d+:([A-Za-z]+)$/.exec(e);return t?`${t[1]} \xB7 ${t[2]}`:e}c($,"x"),s($,"componentOf");export function scriptFamily(e){let t=e.replace(/[-_:][0-9a-f]{6,}(?:-[0-9a-f]+)*$/i,"");return t=t.replace(/[-_:](?=[a-z0-9]*\d)[a-z0-9]{6,}$/i,""),t=t.replace(/[-_:]\d+$/,""),t||e}c(scriptFamily,"scriptFamily"),s(scriptFamily,"scriptFamily");function x(e,t,r,n){const o=t.nodeId,i=o?/^(page|route|ambient):(.*)$/.exec(o):null;if(i)return`${i[1]}:${templatePath(i[2])}`;if(t.type&&I[t.type]==="user-action"&&o)return $(o);if(e==="test")return scriptFamily(r.flowId);const u=r.segments.find(g=>g.kind==="request"&&g.route);if(u?.route)return`${u.method??"GET"} ${templatePath(u.route)}`;const a=n.find(g=>f(g)==="ai-call");return a&&y(a.raw)?`ai \xB7 ${l(a.raw.model)??l(a.raw.provider)??"call"}`:o?templatePath(o):void 0}c(x,"R"),s(x,"subjectOf");const E=10,_=1e3,B=3,N=500,R=1e3;function j(e,t){const r=new Set,n=t.segments.filter(i=>i.kind==="request");t.segments.some(i=>i.counts.errors>0)&&r.add("has-error"),n.some(i=>i.durationMs>=_)&&r.add("slow"),n.length>=E&&r.add("fan-out"),new Set(t.segments.flatMap(i=>i.projects)).size>=3&&r.add("multi-service"),e.some(i=>f(i)==="queue-enqueued")&&r.add("queue"),e.some(i=>f(i)==="ai-call"||i.source==="ai")&&r.add("ai"),t.stats.bookkeeping>=N&&t.stats.bookkeeping*2>=t.stats.events&&r.add("storm"),t.stats.events>=R&&r.add("large");const o=new Map;for(const i of n)(!i.part||i.part.index===1)&&o.set(i.label,(o.get(i.label)??0)+1);return[...o.values()].some(i=>i>=B)&&r.add("repeated-requests"),[...r].sort()}c(j,"F"),s(j,"tagsOf");export function classifyFlow(e,t,r=[]){const n=k(e),o={flowId:t.flowId,origin:n,segments:t.segments},i=j(e,t),u=s((d,O,b)=>{const h=x(d,n,t,e);return{category:d,decidedBy:O,...b!==void 0?{ruleIndex:b}:{},tags:i,origin:n,...h?{subject:h}:{}}},"done"),a=r.findIndex(d=>d.category&&m(d,o));if(a!==-1)return u(r[a].category,"rule",a);if(S(t.flowId,n.nodeId))return u("test","test");const g=n.type?I[n.type]:void 0;if(g)return u(g,"origin");if(i.includes("ai"))return u("ai","shape");const v=t.segments.filter(d=>d.kind==="request");return v.length>0?u(v.every(d=>d.requestId?.startsWith("job:"))?"background":"api","shape"):t.segments.length>0&&t.segments.every(d=>d.kind==="ambient")?u("system","shape"):u("other","shape")}c(classifyFlow,"classifyFlow"),s(classifyFlow,"classifyFlow");export function categoryOfSegment(e,t,r=[]){const n=r.find(o=>o.category&&(o.route!==void 0||o.project!==void 0)&&o.flowId===void 0&&o.origin===void 0&&o.originType===void 0&&m(o,{flowId:e.flowId,origin:t.origin,segments:[e]}));return n?n.category:e.kind==="request"?e.requestId?.startsWith("job:")?"background":"api":e.kind==="ambient"?"system":e.kind==="origin"&&z.has(t.category)?t.category:"ui"}c(categoryOfSegment,"categoryOfSegment"),s(categoryOfSegment,"categoryOfSegment");export function groupFlows(e){return groupByCategory(e).map(t=>{const r=new Map;for(const o of t.items){const i=r.get(o.subject);i?i.push(o):r.set(o.subject,[o])}const n=[...r.entries()].sort((o,i)=>(o[0]===void 0?1:0)-(i[0]===void 0?1:0)||i[1].length-o[1].length||String(o[0]).localeCompare(String(i[0]))).map(([o,i])=>({subject:o,count:i.length,items:i}));return{...t,subjects:n}})}c(groupFlows,"groupFlows"),s(groupFlows,"groupFlows");export function groupByCategory(e){const t=new Map;for(const n of e){const o=t.get(n.category);o?o.push(n):t.set(n.category,[n])}const r=s(n=>{const o=CATEGORY_ORDER.indexOf(n);return o===-1?CATEGORY_ORDER.length:o},"rank");return[...t.entries()].sort((n,o)=>r(n[0])-r(o[0])||(n[0]<o[0]?-1:n[0]>o[0]?1:0)).map(([n,o])=>({category:n,count:o.length,items:o}))}c(groupByCategory,"groupByCategory"),s(groupByCategory,"groupByCategory");
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow segmentation — turn ONE stored flow into the units a person actually reasons about.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. A flow is whatever the browser's ambient window swept up: a page load stamps every
|
|
5
|
+
* fetch, state update and render of its first seconds with one id, so a single flow routinely holds
|
|
6
|
+
* 2–5 K events — a dozen unrelated backend requests, a mount burst, a store hydrating, a second wave
|
|
7
|
+
* after a pause. On real data (72 flows ≥ 500 events from a production-shaped workspace) the median
|
|
8
|
+
* large flow carried 11 independent request trees, and 55 % of its events were registry bookkeeping.
|
|
9
|
+
* Nothing in the pipeline split, bounded or labelled any of that; see `flow-segments.spec.ts`.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT IS NOT. It never changes a stored `flowId`. The browser mints that id, sends it in
|
|
12
|
+
* `x-lensmcp-flow-id`, and the gateway, every pod and every queue job stamp the same string back onto
|
|
13
|
+
* later events — rewriting it at ingest would break that join, and ingest replays a file tail so any
|
|
14
|
+
* rewrite would have to be deterministic across restarts. Segmentation is a DERIVED view, computed at
|
|
15
|
+
* read time, exactly like `buildFlowDetail`.
|
|
16
|
+
*
|
|
17
|
+
* THE MODEL. A flow is cut into SEGMENTS (units) and grouped into PHASES:
|
|
18
|
+
*
|
|
19
|
+
* request one backend call — its browser fetch, every service hop, db/redis/ai work, and the queue
|
|
20
|
+
* worker it enqueued (bridged by `queue#jobId`, whose events carry a different request id)
|
|
21
|
+
* ui a burst of browser events (state updates, renders, mounts) with no request of their own
|
|
22
|
+
* origin the ui burst that holds the `flow-origin` event — what started the flow
|
|
23
|
+
* ambient backend events with no request (event-loop lag, memory samples)
|
|
24
|
+
*
|
|
25
|
+
* A UI burst that begins within `attachWindowMs` after exactly ONE fetch completed is that fetch's
|
|
26
|
+
* response handling and joins its request unit; after SEVERAL it is ambiguous, so it stays its own
|
|
27
|
+
* unit and records which requests it followed (`after`) — guessing would mislead.
|
|
28
|
+
*
|
|
29
|
+
* A PHASE is a run of units separated from the next by an idle gap of `phaseGapMs`. A unit that
|
|
30
|
+
* spans a gap (a queue worker running seconds later, a long stream) keeps the phase connected:
|
|
31
|
+
* phases are computed over unit INTERVALS, never over raw events, so a causal hop is never severed.
|
|
32
|
+
*
|
|
33
|
+
* GUARANTEES (each pinned by a property test): every event is in exactly one segment; order within a
|
|
34
|
+
* segment is time order; the result depends only on the SET of events (arrival order is irrelevant);
|
|
35
|
+
* ids are stable — appending later events never moves an existing event to another segment; a request
|
|
36
|
+
* is never split across segments except by the size cap; no segment exceeds the cap.
|
|
37
|
+
*
|
|
38
|
+
* Pure and I/O-free. Cost is O(n log n) in the number of events.
|
|
39
|
+
*/
|
|
40
|
+
import type { BaseEvent } from '@lensmcp/protocol-types';
|
|
41
|
+
export type SegmentKind = 'origin' | 'request' | 'ui' | 'ambient';
|
|
42
|
+
export interface SegmentOptions {
|
|
43
|
+
/** An idle gap this long between units starts a new phase. Default 1500 ms. */
|
|
44
|
+
phaseGapMs?: number;
|
|
45
|
+
/** Browser events further apart than this are different bursts. Default 250 ms. */
|
|
46
|
+
burstGapMs?: number;
|
|
47
|
+
/** A burst starting this soon after a fetch completed is that fetch's response handling. Default 400 ms. */
|
|
48
|
+
attachWindowMs?: number;
|
|
49
|
+
/** A segment with more signal events than this is split into time-ordered parts. Default 400. */
|
|
50
|
+
maxSegmentEvents?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Two outermost hops of one request id closer than this are one call; further apart, two calls.
|
|
53
|
+
* Only matters for a script that reuses one `x-request-id` for every call. Default 10 ms.
|
|
54
|
+
*/
|
|
55
|
+
occurrenceSlackMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare const DEFAULT_SEGMENT_OPTIONS: Required<SegmentOptions>;
|
|
58
|
+
/**
|
|
59
|
+
* Registry bookkeeping: causality-window noise, never mechanism. Mirrors the list the flow reducer
|
|
60
|
+
* refuses at admission and `buildFlowDetail` filters — three copies of one definition, kept equal by
|
|
61
|
+
* `flow-segments.spec.ts` so they cannot drift apart silently.
|
|
62
|
+
*/
|
|
63
|
+
export declare const FLOW_BOOKKEEPING_KINDS: ReadonlySet<string>;
|
|
64
|
+
/** Which slice of an over-cap unit a segment is: `index` of `total`, 1-based. */
|
|
65
|
+
export interface SegmentPart {
|
|
66
|
+
index: number;
|
|
67
|
+
total: number;
|
|
68
|
+
}
|
|
69
|
+
export interface SegmentCounts {
|
|
70
|
+
total: number;
|
|
71
|
+
/** Everything that is not registry bookkeeping. */
|
|
72
|
+
signal: number;
|
|
73
|
+
bookkeeping: number;
|
|
74
|
+
errors: number;
|
|
75
|
+
warnings: number;
|
|
76
|
+
}
|
|
77
|
+
export interface FlowSegment {
|
|
78
|
+
/** Stable: `<flowId>~r:<requestId>[@n]` · `~u:<firstEventId>` · `~o:<firstEventId>` · `~a:<firstEventId>`; parts add `#n`. */
|
|
79
|
+
id: string;
|
|
80
|
+
flowId: string;
|
|
81
|
+
kind: SegmentKind;
|
|
82
|
+
label: string;
|
|
83
|
+
/** 1-based, in time order. Units in the same phase happened in the same burst of activity. */
|
|
84
|
+
phase: number;
|
|
85
|
+
/** Earliest moment the segment was active (an event's own `startedAt` counts, not just its stamp). */
|
|
86
|
+
start: number;
|
|
87
|
+
end: number;
|
|
88
|
+
durationMs: number;
|
|
89
|
+
/** Every event in the segment, in time order, bookkeeping included. */
|
|
90
|
+
eventIds: string[];
|
|
91
|
+
counts: SegmentCounts;
|
|
92
|
+
projects: string[];
|
|
93
|
+
/** `METHOD /route` of the request, when there is one. */
|
|
94
|
+
route?: string;
|
|
95
|
+
method?: string;
|
|
96
|
+
requestId?: string;
|
|
97
|
+
/** For a UI burst that followed several fetches: which requests it may belong to. */
|
|
98
|
+
after?: string[];
|
|
99
|
+
/** For a UI burst: what churned most — components that rendered, store paths that changed. Most active first. */
|
|
100
|
+
hot?: string[];
|
|
101
|
+
/** Set when a request or burst was cut by the size cap. */
|
|
102
|
+
part?: SegmentPart;
|
|
103
|
+
worstSeverity: 'debug' | 'info' | 'warning' | 'error' | 'fatal';
|
|
104
|
+
}
|
|
105
|
+
export interface FlowPhase {
|
|
106
|
+
index: number;
|
|
107
|
+
start: number;
|
|
108
|
+
end: number;
|
|
109
|
+
segmentIds: string[];
|
|
110
|
+
}
|
|
111
|
+
export interface FlowSegmentation {
|
|
112
|
+
flowId: string;
|
|
113
|
+
segments: FlowSegment[];
|
|
114
|
+
phases: FlowPhase[];
|
|
115
|
+
/** eventId → segment id. Total: no event is missing and none appears twice. */
|
|
116
|
+
assignment: Record<string, string>;
|
|
117
|
+
stats: {
|
|
118
|
+
events: number;
|
|
119
|
+
signal: number;
|
|
120
|
+
bookkeeping: number;
|
|
121
|
+
segments: number;
|
|
122
|
+
phases: number;
|
|
123
|
+
largestSegment: number;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export declare function segmentFlow(input: readonly BaseEvent[], options?: SegmentOptions): FlowSegmentation;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var z=Object.defineProperty;var q=(e,t)=>z(e,"name",{value:t,configurable:!0});var L=Object.defineProperty,h=q((e,t)=>L(e,"name",{value:t,configurable:!0}),"h");export const DEFAULT_SEGMENT_OPTIONS={phaseGapMs:1500,burstGapMs:250,attachWindowMs:400,maxSegmentEvents:400,occurrenceSlackMs:10},FLOW_BOOKKEEPING_KINDS=new Set(["subscription-added","subscription-removed","hook-run","store-created","singleton-instance","visual-frame"]);const P=new Set(["react","valtio","client-runtime","chrome","visual"]),y=h(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isObj"),m=h(e=>typeof e=="string"&&e!==""?e:void 0,"str"),M=h(e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,"num");function E(e){const t=y(e.raw)?e.raw:{},o=y(t.request)?t.request:{},r=y(t.span)?t.span:{},u=e.context??{},n=M(e.timestamp)??0,a=m(t.kind)??"",c=M(o.startedAt)??M(r.startTime)??M(t.startedAt),g=M(o.endedAt)??M(r.endTime)??(c!==void 0&&M(t.durationMs)!==void 0?c+t.durationMs:void 0),w=Math.min(n,c??n),k=Math.max(n,g??n),b=y(t.render)?t.render:{},d=y(t.update)?t.update:{},i=a==="render"?m(b.componentName):a==="state-update"?m(d.path)?.split(".").slice(0,2).join("."):void 0,I=m(t.queue),j=m(t.jobId),s=String(e.source),f=m(t.project)??m(u.projectName);return{event:e,id:e.id,ts:n,from:w,to:k,kind:a,source:s,...m(u.requestId)?{requestId:m(u.requestId)}:{},...m(u.parentRequestId)?{parentRequestId:m(u.parentRequestId)}:{},...I&&j?{queueKey:`${I}#${j}`}:{},browser:P.has(s)||s==="memory"&&f===void 0,bookkeeping:FLOW_BOOKKEEPING_KINDS.has(a),...f?{project:f}:{},...i?{subject:i}:{},...a==="fetch-start"||a==="fetch-done"?{route:x(m(t.url)),method:m(t.method)}:a==="server-request"?{route:m(o.route),method:m(o.method)}:a==="gateway-request"?{route:x(m(t.url)),method:m(t.method)}:{}}}q(E,"F"),h(E,"readItem");function x(e){if(!e)return;const t=e.search(/[?#]/),o=t===-1?e:e.slice(0,t),r=/^[a-z][a-z0-9+.-]*:\/\/[^/]*(\/.*)?$/i.exec(o);return r?r[1]??"/":o}q(x,"S"),h(x,"pathOnly");const C=h(e=>e.split(".")[0],"rootOf"),N=h((e,t)=>e.reduce((o,r)=>Math.min(o,t(r)),1/0),"minOf"),F=h((e,t)=>e.reduce((o,r)=>Math.max(o,t(r)),-1/0),"maxOf"),O=h((e,t)=>e.ts-t.ts||(e.id<t.id?-1:e.id>t.id?1:0),"compare"),S=h(e=>e.requestId&&!e.requestId.startsWith("job:")?C(e.requestId):void 0,"directFamilyOf");function G(e){if(e.requestId?.startsWith("job:"))return e.requestId.slice(4);if(e.queueKey&&e.kind!=="queue-enqueued"&&!e.requestId)return e.queueKey}q(G,"W"),h(G,"jobKeyOf");function A(e,t){const o=e.filter(n=>!n.parentRequestId&&(n.kind==="server-request"||n.kind==="gateway-request"||n.kind==="fetch-start"||n.kind==="fetch-done")).sort((n,a)=>n.from-a.from||O(n,a));if(o.length<2)return[e];const r=[];for(const n of o){const a=r[r.length-1];a&&n.from<=a.to+t?a.to=Math.max(a.to,n.to):r.push({from:n.from,to:n.to})}if(r.length<2)return[e];const u=r.map(()=>[]);for(const n of e){let a=r.findIndex(c=>n.ts>=c.from-t&&n.ts<=c.to+t);if(a===-1){a=0;for(let c=0;c<r.length;c+=1)r[c].from<=n.ts&&(a=c)}u[a].push(n)}return u.filter(n=>n.length>0)}q(A,"T"),h(A,"occurrencesOf");const W={debug:0,info:1,warning:2,error:3,fatal:4};function K(e,t){const o=[];let r=[];for(const u of e){const n=r[r.length-1];n&&u.ts-n.ts>=t&&(o.push(r),r=[]),r.push(u)}return r.length&&o.push(r),o}q(K,"v"),h(K,"runs");export function segmentFlow(e,t={}){const o={...DEFAULT_SEGMENT_OPTIONS};for(const[s,f]of Object.entries(t))typeof f=="number"&&Number.isFinite(f)&&(o[s]=f);const r=new Set,u=[];for(const s of e)r.has(s.id)||(r.add(s.id),u.push(E(s)));u.sort(O);const n=m(u[0]?.event.context?.flowId)??m(u[0]?.event.raw?.flowId)??"",a=new Map;for(const s of u)s.kind==="queue-enqueued"&&s.queueKey&&s.requestId&&!s.requestId.startsWith("job:")&&!a.has(s.queueKey)&&a.set(s.queueKey,s);const c=new Map,g=new Map,w=[];for(const s of u){const f=G(s),p=S(s);if(f!==void 0){const l=g.get(f);l?l.push(s):g.set(f,[s])}else if(p!==void 0){const l=c.get(p);l?l.push(s):c.set(p,[s])}else w.push(s)}const k=[],b=new Map;for(const[s,f]of c)A(f,o.occurrenceSlackMs).forEach((p,l)=>{const v={id:`${n}~r:${s}${l===0?"":`@${l+1}`}`,kind:"request",items:p,requestId:s};k.push(v);for(const $ of p)b.set($,v)});for(const[s,f]of g){const p=a.get(s),l=p?b.get(p):void 0;l?l.items.push(...f):k.push({id:`${n}~r:job:${s}`,kind:"request",items:f,requestId:`job:${s}`})}const d=u.filter(s=>s.kind==="fetch-done"&&S(s)!==void 0),i=new Map;for(const s of k)s.requestId&&i.set(s.requestId,[...i.get(s.requestId)??[],s]);const I=h((s,f)=>{const p=i.get(s);if(p)return p.find(l=>l.items.some(v=>v.kind==="fetch-done"&&v.ts===f))??p[p.length-1]},"owningUnit"),j=w.filter(s=>s.browser);for(const s of K(j,o.burstGapMs)){const f=s[0].ts,p=d.filter($=>$.ts<=f&&f-$.ts<=o.attachWindowMs),l=[...new Set(p.map($=>S($)))];if(l.length===1){const $=I(l[0],p[p.length-1].ts);if($){$.items.push(...s);continue}}const v=s.some($=>$.kind==="flow-origin");k.push({id:`${n}~${v?"o":"u"}:${s[0].id}`,kind:v?"origin":"ui",items:s,...l.length>1?{after:l.sort()}:{}})}for(const s of K(w.filter(f=>!f.browser),o.phaseGapMs))k.push({id:`${n}~a:${s[0].id}`,kind:"ambient",items:s});return B(n,u.length,k,o)}q(segmentFlow,"segmentFlow"),h(segmentFlow,"segmentFlow");function B(e,t,o,r){for(const d of o)d.items.sort(O);const u=o.map(d=>({unit:d,from:N(d.items,i=>i.from),to:F(d.items,i=>i.to)}));u.sort((d,i)=>d.from-i.from||(d.unit.id<i.unit.id?-1:1));const n=new Map;let a=0,c=-1/0;for(const d of u)(a===0||d.from-c>=r.phaseGapMs)&&(a+=1),c=Math.max(c,d.to),n.set(d.unit,a);const g=[];for(const d of u){const i=R(d.unit.items,r.maxSegmentEvents);i.forEach((I,j)=>{g.push(T(e,d.unit,I,n.get(d.unit),i.length>1?{index:j+1,total:i.length}:void 0))})}const w={};for(const d of g)for(const i of d.eventIds)w[i]=d.id;const k=[];for(const d of g){let i=k[d.phase-1];i||(i={index:d.phase,start:d.start,end:d.end,segmentIds:[]},k[d.phase-1]=i),i.start=Math.min(i.start,d.start),i.end=Math.max(i.end,d.end),i.segmentIds.push(d.id)}const b=g.reduce((d,i)=>d+i.counts.signal,0);return{flowId:e,segments:g,phases:k.filter(Boolean),assignment:w,stats:{events:t,signal:b,bookkeeping:t-b,segments:g.length,phases:k.filter(Boolean).length,largestSegment:g.reduce((d,i)=>Math.max(d,i.counts.total),0)}}}q(B,"_"),h(B,"assemble");function R(e,t){if(e.filter(n=>!n.bookkeeping).length<=t)return[e];const o=[];let r=[],u=0;for(const n of e)!n.bookkeeping&&u===t&&(o.push(r),r=[],u=0),r.push(n),n.bookkeeping||(u+=1);return r.length&&o.push(r),o}q(R,"U"),h(R,"chunk");function T(e,t,o,r,u){const n={total:o.length,signal:0,bookkeeping:0,errors:0,warnings:0},a=new Set;let c="debug";for(const i of o){i.bookkeeping?n.bookkeeping+=1:n.signal+=1;const I=String(i.event.severity);I==="error"||I==="fatal"?n.errors+=1:I==="warning"&&(n.warnings+=1),(W[I]??1)>(W[c]??0)&&(c=I),i.project&&a.add(i.project)}const g=t.kind==="ui"||t.kind==="origin"?_(o):[],w=N(o,i=>i.from),k=F(o,i=>i.to),b=U(t.items),d=t.id;return{id:u&&u.index>1?`${d}#${u.index}`:d,flowId:e,kind:t.kind,label:D(t,o,b,u,g),phase:r,start:w,end:k,durationMs:k-w,eventIds:o.map(i=>i.id),counts:n,projects:[...a].sort(),...b.route?{route:b.route}:{},...b.method?{method:b.method}:{},...t.requestId?{requestId:t.requestId}:{},...t.after?{after:t.after}:{},...g.length?{hot:g}:{},...u?{part:u}:{},worstSeverity:c}}q(T,"B"),h(T,"toSegment");function U(e){const t=e.find(o=>o.kind==="fetch-start"&&o.route)??e.find(o=>o.kind==="server-request"&&!o.parentRequestId&&o.route)??e.find(o=>o.kind==="gateway-request"&&o.route)??e.find(o=>o.kind==="fetch-done"&&o.route);return{...t?.route?{route:t.route}:{},...t?.method?{method:t.method.toUpperCase()}:{}}}q(U,"D"),h(U,"routeOf");function _(e){const t=new Map;for(const o of e)o.subject&&!o.bookkeeping&&t.set(o.subject,(t.get(o.subject)??0)+1);return[...t.entries()].sort((o,r)=>r[1]-o[1]||(o[0]<r[0]?-1:1)).slice(0,3).map(([o,r])=>r>1?`${o} \xD7${r}`:o)}q(_,"L"),h(_,"hotOf");function D(e,t,o,r,u){const n=r?` (part ${r.index}/${r.total})`:"";if(e.kind==="request"){if(o.route)return`${o.method?`${o.method} `:""}${o.route}${n}`;const a=t.find(c=>c.queueKey);return a?`job ${a.queueKey}${n}`:`request ${e.requestId??""}${n}`}return e.kind==="origin"?`${t.find(a=>a.kind==="flow-origin")?.event.title||"start"}${u.length?` \xB7 ${u.join(", ")}`:""}${n}`:e.kind==="ui"?`UI update${u.length?` \xB7 ${u.join(", ")}`:""}${n}`:`background${n}`}q(D,"z"),h(D,"labelOf");
|
package/lib/flow-shape.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var ge=Object.defineProperty;var B=(d,w)=>ge(d,"name",{value:w,configurable:!0});var $e=Object.defineProperty,g=B((d,w)=>$e(d,"name",{value:w,configurable:!0}),"h");import{redactDeep as ye,redactText as de}from"./redact.js";import{boundedClone as ke}from"@lensmcp/protocol-types";export function areaOf(d){const{source:w,category:o,title:M,kind:y}=d;return w==="ai"?"ai":y?.startsWith("memory")||y==="db-query"?"data":w==="gateway"||y==="gateway-request"?"gateway":y==="queue-process"||y==="queue-active"||y==="queue-completed"||y==="queue-failed"?"services":w==="db"||w==="redis"||w==="queue"||y==="redis-op"||y?.startsWith("queue-")?"data":w==="external"||y==="egress"?"network":w==="nestjs"||w==="nest"?/(?:Repository|Repo|Dao|Store)\./.test(M)?"data":"services":w==="valtio"?"state":o==="network"?"network":"browser"}B(areaOf,"areaOf"),g(areaOf,"areaOf");const ue=240,qe=new Set(["systemPrompt","prompt","prompts","messages","completion","completions","input","output","draft","text","content","result","arguments","params"]),we=new Set(["tenantId","caller","userId","accountId","email","subject"]),Me=8;function ce(d){const w=Q(d,0);return ke(ye({redacted:!0,...w}),{rawByteLimit:16*1024}).value}B(ce,"$t"),g(ce,"aiProjection");function Q(d,w){if(typeof d=="string")return fe(d);if(d===null||typeof d!="object")return d;if(w>=Me)return;if(Array.isArray(d))return d.slice(0,32).map(M=>Q(M,w+1));const o={};for(const[M,y]of Object.entries(d))if(!we.has(M)){if(qe.has(M)){typeof y=="string"?o[`${M}Chars`]=y.length:Array.isArray(y)?o[`${M}Count`]=y.length:y!=null&&(o[`${M}Present`]=!0);continue}o[M]=Q(y,w+1)}return o}B(Q,"tt"),g(Q,"projectAi");function fe(d){return d.length<=ue?de(d):`${de(d.slice(0,ue))}\u2026`}B(fe,"wt"),g(fe,"clipText");export function trimFlowEvent(d,w={}){const o=d.raw??{},M=o.render??{},y=o.span??{},E=o.request??{},p=g(c=>typeof c=="number"?c:void 0,"num"),x=o.loop??{},D=o.effect??{},O=p(o.durationMs)??p(y.durationMs)??p(E.durationMs)??p(x.durationMs)??p(M.actualDurationMs)??p(D.durationMs),m=typeof o.kind=="string"?o.kind:void 0,U=m==="fetch-done"||m==="fetch-error",z=p(M.commitTime),T=m==="render"&&z!==void 0&&Math.abs(z-d.timestamp)<6e4,K=U?d.timestamp:T?z:p(o.startedAt)??p(y.startTime)??p(E.startedAt)??p(x.startedAt)??(O!==void 0?d.timestamp-O:d.timestamp);let b;if(typeof o.query=="string"){const c=Array.isArray(o.params)?` [${o.params.join(", ")}]`:"",q=typeof o.result=="string"?` \u2192 ${o.result}`:"";b=o.query+c+q}else if(typeof o.command=="string"){const c=typeof o.value=="string"?` \u2192 ${o.value}`:o.hit===!1?" \u2192 (miss)":"";b=o.command+c}else if(m==="state-update"){const c=o.update??{};let q="";try{q=JSON.stringify(c.preview)}catch{}b=`${c.storeId??""}.${c.path??""} = ${q}`}else m==="queue-completed"&&typeof o.returned=="string"?b=`returned ${o.returned}`:m==="queue-enqueued"&&(b=`${o.job??"job"}#${o.jobId??"?"} \u2192 ${o.queue??""}${typeof o.data=="string"?" "+o.data:""}`);let F;if(m==="gateway-request")F=`${o.host??""}${String(o.url??"").split("?")[0]}`;else if(m==="fetch-start"||m==="fetch-done"||m==="fetch-error")try{const c=new URL(String(o.href??o.url??""));F=`${c.hostname}${c.pathname}`}catch{}let P;if(m==="gateway-request"&&Array.isArray(o.steps))P=o.steps.map(c=>{const q=[String(c.step)];return c.project&&q.push(String(c.project)),c.prefix&&q.push(String(c.prefix)),c.mode&&q.push(String(c.mode)+(c.ok===!1?" \u2717":c.ok===!0?" \u2713":"")),c.caller&&q.push("by "+String(c.caller)),c.pod&&q.push(String(c.pod)),typeof c.atMs=="number"&&q.push(`@${c.atMs}ms`),q.join(" ")}),b=P.map(c=>c.replace(/ @\d+ms$/,"")).join(" \u2192 ");else if(m==="egress")b=`${o.method??"GET"} ${o.host??""} \u2192 ${o.status??"?"}`;else if(d.source==="ai"){if(m==="ai-round"){const c=o.model??{},q=o.usage??{},A=Array.isArray(o.toolCalls)?o.toolCalls:[],V=`${p(q.promptTokens)??"?"}\u2192${p(q.completionTokens)??"?"} tok`,H=A.length?` \xB7 ${A.length} tool call${A.length===1?"":"s"}`:"",X=p(o.durationMs)!==void 0?` \xB7 ${(p(o.durationMs)/1e3).toFixed(1)}s`:"";b=`${c.provider??""}/${c.model??""} \xB7 ${V}${H}${X}`}else if(m==="ai-turn-start"){const c=Array.isArray(o.tools)?o.tools:[];b=`skill ${o.skill??""} \xB7 ${o.output??""}${c.length?" \xB7 tools: "+c.join(", "):""}`}else if(m==="ai-tool-call"){const c=o.call??{},q=o.result??{},A=p(o.durationMs)!==void 0?` \xB7 ${(p(o.durationMs)/1e3).toFixed(1)}s`:"";b=`${c.name??"tool"} ${q.error!=null?"\u2717 "+String(q.error):"\u2713"}${A}`}else if(m==="ai-turn-end"){const c=o.usage??{},q=p(o.rounds)!==void 0?` \xB7 ${p(o.rounds)} round${p(o.rounds)===1?"":"s"}`:"",A=p(o.durationMs)!==void 0?` \xB7 ${(p(o.durationMs)/1e3).toFixed(1)}s`:"";b=`${o.ok===!1?"FAILED":"ok"} \xB7 ${p(c.promptTokens)??"?"}+${p(c.completionTokens)??"?"} tok${q}${A}`}}let S;m==="db-query"&&typeof o.query=="string"?S=o.query:m==="redis-op"?S=`${o.op??""} ${String(o.key??"").replace(/\d+/g,"#")}`:m==="egress"?S=`${o.method??"GET"} ${o.host??""}`:m==="queue-enqueued"?S=`enqueue ${o.queue??""}`:m==="span"&&typeof y.name=="string"?S=y.name:m==="memory-mutation"&&(S=d.title);const R=d.context??{},G=M.commitId??D.commitId??o.commitId,h=Array.isArray(M.why)?M.why.map(c=>typeof c?.type=="string"?c.type:void 0).filter(c=>!!c):[];return{id:d.id,t:d.timestamp,start:K,...S?{sig:S}:{},...F?{urlKey:F}:{},...typeof o.queue=="string"?{queue:o.queue}:{},...o.jobId!=null?{jobId:String(o.jobId)}:{},source:d.source,category:d.category,severity:d.severity,title:d.title,kind:m,area:areaOf({source:d.source,category:d.category,title:d.title,kind:m}),...typeof y.role=="string"?{role:y.role}:{},...typeof R.requestId=="string"?{requestId:R.requestId}:{},...typeof R.parentRequestId=="string"?{parentRequestId:R.parentRequestId}:{},...typeof G=="string"?{commitId:G}:{},...typeof M.componentName=="string"?{component:M.componentName}:{},...typeof M.phase=="string"?{phase:M.phase}:{},...h.length?{why:h}:{},...m==="effect-run"&&typeof D.hookLogicalId=="string"?{hook:D.hookLogicalId}:{},...m==="effect-run"&&D.cleanupRan===!0?{cleanup:!0}:{},...m==="render-commit-truncated"?{renderTotal:p(o.total),renderPublished:p(o.published)}:{},...typeof R.projectName=="string"?{project:R.projectName}:typeof o.project=="string"?{project:o.project}:{},...O!==void 0?{durationMs:O}:{},...U&&p(o.startedAt)!==void 0?{fetchStartedAt:p(o.startedAt)}:{},...d.source==="ai"?{ai:w.includeRaw?o:ce(o)}:{},...b?{detail:b}:{},...P?{steps:P}:{}}}B(trimFlowEvent,"trimFlowEvent"),g(trimFlowEvent,"trimFlowEvent");const le=1;function pe(d){return d==="flow-origin"?0:d?.endsWith("-start")?1:d==="gateway-request"?2:d==="server-request"?3:d==="span"?4:d==="render"||d==="render-commit-truncated"?6:d==="effect-run"?7:5}B(pe,"kt"),g(pe,"kindRank");function C(d){return d.kind==="span"&&d.role==="controller"?3.5:d.kind==="queue-process"?3:pe(d.kind)}B(C,"W"),g(C,"causalRank");export function buildFlowDetail(d,w={}){const o=d.slice().filter(e=>{const t=e.raw??{},r=typeof t.kind=="string"?t.kind:"";if(r==="subscription-added"||r==="subscription-removed"||r==="hook-run"||r==="store-created"||r==="singleton-instance")return!1;const n=t.span;return!(typeof n?.name=="string"&&n.name.startsWith("nest.request "))}).map(e=>trimFlowEvent(e,w)),M=new Set;for(let e=o.length-1;e>=0;e--)M.has(o[e].id)?o.splice(e,1):M.add(o[e].id);const y=new Map;for(const e of o)(e.kind==="gateway-request"||e.kind==="queue-process")&&e.requestId&&!y.has(e.requestId)&&y.set(e.requestId,e.start);const E=g(e=>e.source!=="react"&&e.source!=="valtio"&&e.requestId&&y.has(e.requestId)?y.get(e.requestId):e.start,"anchorOf");o.sort((e,t)=>E(e)-E(t)||e.start-t.start||C(e)-C(t)||(t.durationMs??0)-(e.durationMs??0)||e.t-t.t);const p=g(e=>(e.kind==="span"||e.kind==="server-request"||e.kind==="gateway-request"||e.kind==="queue-process")&&(e.durationMs??0)>0,"spanLike"),x=o;for(const e of x)e.group=e.source==="react"||e.source==="valtio"||e.source==="client-runtime"||e.source==="visual"?"browser":"server";const D=new Map;for(const e of x)if((e.kind==="gateway-request"||e.kind==="queue-process")&&e.requestId&&e.project){const t=D.get(e.requestId)??[];t.push({project:e.project,start:e.start,end:e.start+(e.durationMs??0)}),D.set(e.requestId,t)}const O=g((e,t)=>{const r=D.get(e);if(r?.length)return r.every(n=>n.project===r[0].project)?r[0].project:r.filter(n=>n.start<=t+1&&n.end>=t-1).sort((n,s)=>s.start-n.start)[0]?.project},"projectOf");for(const e of x)if(!e.project&&e.source==="nestjs"&&e.requestId){const t=O(e.requestId,e.start);t&&(e.project=t)}for(const e of x){let t;const r=e.requestId;for(const n of x){if(n===e||!p(n)||n.group!==e.group||n.requestId!==r)continue;const s=n.project,u=e.project;if(n.kind!=="gateway-request"&&e.kind!=="gateway-request"&&s!==void 0&&u!==void 0&&s!==u)continue;const l=n.durationMs??0,$=e.durationMs??0,a=Math.abs(n.start-e.start)<=le,i=C(n),f=C(e),k=n.start<=e.start;if(!(n.start+l>e.start&&(k||a&&i<f)&&(!a||(i!==f?i<f:k&&l>$))))continue;const j=t?C(t):-1,I=t?Math.abs(n.start-t.start)<=le:!1;(!t||!I&&n.start>t.start||I&&i>j||I&&i===j&&(n.durationMs??0)<(t.durationMs??0))&&(t=n)}t&&(e.parent=t.id)}const m=new Map;for(const e of x){if(!e.requestId)continue;const t=m.get(e.requestId);t?t.push(e):m.set(e.requestId,[e])}const U=new Map(x.map(e=>[e.id,e])),z=g((e,t)=>{const r=(m.get(e)??[]).filter(n=>p(n)&&n.start<=t&&n.start+(n.durationMs??0)>=t);if(r.length!==0)return r.sort((n,s)=>s.start-n.start||(n.durationMs??0)-(s.durationMs??0)),r[0]},"callerNodeFor");for(const e of x){if(!e.parentRequestId||e.kind!=="server-request"&&e.kind!=="gateway-request")continue;const t=e.parent?U.get(e.parent):void 0;if(t&&t.requestId===e.requestId)continue;const r=z(e.parentRequestId,e.start);r&&r!==e&&r.start<=e.start&&(e.parent=r.id)}const T=x,K=new Map;for(const e of T)e.kind==="gateway-request"&&e.requestId&&!K.has(e.requestId)&&K.set(e.requestId,e);for(const e of T)if(e.group==="server"&&!e.parent&&e.requestId&&e.kind!=="gateway-request"){const t=K.get(e.requestId);t&&t!==e&&t.start<=e.start&&(e.parent=t.id)}const b=new Set,F=new Set,P=[];for(const e of T){if(e.kind!=="gateway-request")continue;if(!e.requestId){P.push(e);continue}const t=m.get(e.requestId)??[];if(t.filter(s=>s.kind==="gateway-request").sort((s,u)=>s.start-u.start)[0]!==e)continue;const r=t.find(s=>s.kind==="fetch-start"),n=t.find(s=>s.kind==="fetch-done"||s.kind==="fetch-error");if(!r&&!n){P.push(e);continue}r&&(e.fetchStartId=r.id,b.add(r.id)),n&&(e.fetchDoneId=n.id,F.add(n.id))}const S=new Map;for(const e of T){if(!e.urlKey)continue;const t=S.get(e.urlKey)??{gws:[],starts:[],dones:[]};e.kind==="gateway-request"?P.includes(e)&&t.gws.push(e):e.kind==="fetch-start"?b.has(e.id)||t.starts.push(e):(e.kind==="fetch-done"||e.kind==="fetch-error")&&(F.has(e.id)||t.dones.push(e)),S.set(e.urlKey,t)}for(const{gws:e,starts:t,dones:r}of S.values()){e.sort((s,u)=>s.start-u.start),t.sort((s,u)=>s.start-u.start);const n=new Set;e.forEach((s,u)=>{const l=t[u];l&&(s.fetchStartId=l.id);const $=l?l.t:s.start;let a,i=1/0;for(const f of r){if(n.has(f.id))continue;const k=f.fetchStartedAt??f.start-(f.durationMs??0),j=Math.abs(k-$);j<i&&(i=j,a=f)}a&&(s.fetchDoneId=a.id,n.add(a.id))})}const R=new Map,G=new Map;for(const e of T)!e.queue||!e.jobId||(e.kind==="queue-enqueued"&&R.set(`${e.queue}#${e.jobId}`,e),e.kind==="queue-process"&&G.set(`${e.queue}#${e.jobId}`,e));for(const e of T){if(!e.queue||!e.jobId)continue;const t=`${e.queue}#${e.jobId}`;if(e.kind==="queue-process"&&!e.parent){const r=R.get(t);r&&r!==e&&r.start<=e.start&&(e.parent=r.id)}else if(e.kind==="queue-active"||e.kind==="queue-completed"||e.kind==="queue-failed"){const r=G.get(t);r&&r!==e&&r.start<=e.start&&(e.parent=r.id)}}let h=x;{const e=new Map;for(const r of h)r.kind==="fetch-start"&&e.set(r.id,r);const t=new Map;for(const r of h){if(r.kind!=="fetch-done"&&r.kind!=="fetch-error")continue;const n=[...e.values()].find(l=>l.requestId&&l.requestId===r.requestId)?.id,s=n?e.get(n):void 0;if(!s)continue;const u=r.fetchStartedAt??s.start;s.title=r.title,s.severity=r.severity,s.durationMs=r.durationMs??Math.max(0,r.t-u),s.kind="fetch",r.detail&&!s.detail&&(s.detail=r.detail),t.set(r.id,s.id)}if(t.size>0){h=h.filter(r=>!t.has(r.id));for(const r of h)for(r.fetchDoneId&&t.has(r.fetchDoneId)&&(r.fetchDoneId=t.get(r.fetchDoneId));r.parent&&t.has(r.parent);)r.parent=t.get(r.parent)}}const c=g(e=>!!e.commitId&&(e.kind==="render"||e.kind==="effect-run"||e.kind==="render-commit-truncated"),"commitMember"),q=g(e=>[...new Set(e)],"uniq"),A=g(e=>q(e.map(t=>t.component).filter(t=>!!t)),"named"),V=g(e=>e.length<=2?e.join(" + "):`${e[0]} + ${e.length-1} more`,"nameList"),H=g(e=>{const t=new Map;for(const r of e)r.component&&t.set(r.component,(t.get(r.component)??0)+1);return[...t].map(([r,n])=>n>1?`${r} \xD7${n}`:r).join(", ")},"tally"),X=g(e=>{if(!e)return"effect";const t=/([^/\\]+?):(\d+)(?::\d+)?$/.exec(e);return t?`${t[1]}:${t[2]}`:e},"shortHook"),Y=g(e=>`${X(e.hook)}${e.cleanup?" (cleanup)":""}`,"hookLabel"),Z=g(e=>{const t=Math.min(...e.map(n=>n.start)),r=Math.max(...e.map(n=>n.start+(n.durationMs??0)));return{start:t,durationMs:r-t}},"extent"),ee=g(e=>(delete e.component,delete e.why,delete e.phase,delete e.hook,delete e.cleanup,delete e.renderTotal,delete e.renderPublished,e),"asGroup");function te(e){const t=e[0],r=e.filter(v=>v.kind==="render"),n=e.filter(v=>v.kind==="effect-run"),s=e.find(v=>v.kind==="render-commit-truncated"),u=[...r].sort((v,W)=>(W.durationMs??0)-(v.durationMs??0)),l=u.filter(v=>(v.why??[]).some(W=>W!=="parent-render")),$=q(l.flatMap(v=>(v.why??[]).filter(W=>W!=="parent-render"))),a=A(l),i=r.filter(v=>v.phase==="mount"),f=r.filter(v=>v.phase!=="mount"&&!l.includes(v)),k=u[0]?.component??"view",j=r.length>0&&i.length===r.length,I=$.length?$.join(" + "):void 0,L=j?"mount":"update",N=a.length?`view update \xB7 ${V(a)} \u2190 ${I}`:j?`view mount \xB7 ${k}`:`view update \xB7 ${k}`,ae=s?.renderTotal??r.length,he=[i.length?`mounted ${H(i)}`:"",f.length?`re-rendered ${H(f)}`:"",!a.length&&!j&&r.length?"trigger not recorded \u2014 every captured render came from its parent":"",s?`${ae} renders, ${s.renderPublished??r.length} recorded (per-commit cap)`:`${r.length} render${r.length===1?"":"s"}`,n.length?`${n.length} effect${n.length===1?"":"s"}: ${n.map(Y).join(", ")}`:""].filter(Boolean).join(" \xB7 "),ie=A(r);return ee({...t,...Z(e),kind:"view-update",title:N,detail:he,commitId:t.commitId,viewPhase:L,...I?{cause:I}:{},causeComponents:a,components:ie,mounted:A(i),renderCount:ae,recordedRenders:r.length,effects:n.length,effectHooks:n.map(Y),memberIds:e.map(v=>v.id),sig:`commit:${L}:${I??"-"}:${a.join(",")}:${ie.slice().sort().join(",")}`,severity:e.some(v=>v.severity==="error")?"error":t.severity})}B(te,"ct"),g(te,"viewUpdateNode");function re(e){const t=e[0],r=e.map(Y);return ee({...t,...Z(e),kind:"commit-effects",title:e.length===1?`effect \xB7 ${r[0]}`:`effects \xB7 ${r[0]} + ${e.length-1} more`,detail:r.join(", "),commitId:t.commitId,effects:e.length,effectHooks:r,memberIds:e.map(n=>n.id),sig:`effects:${r.slice().sort().join(",")}`})}B(re,"dt"),g(re,"effectsNode");const me=g(e=>q(e.map(t=>t==="state-update"?"state update":t==="fetch-start"?"request":t==="flow-origin"?"interaction":"response")).join(" + "),"describeInputs");function ne(e,t){const r=e[0],n=e[e.length-1],s=e.length,u=r.detail,l=Math.min(...e.map(f=>f.start)),$=Math.max(...e.map(f=>f.start+(f.durationMs??0))),a=Math.round(e.reduce((f,k)=>f+(k.durationMs??0),0)/s*10)/10,i=Math.round(($-l)*10)/10;r.count=s,r.avgMs=a,r.wallMs=i,r.durationMs=$-l,r.items=e.slice(0,12).map(f=>({start:f.start-l,...f.durationMs!=null?{durationMs:f.durationMs}:{}})),r.memberIds=e.flatMap(f=>f.memberIds??[f.id]),r.title=`\xD7${s} ${r.title}`,e.some(f=>f.severity==="error")&&(r.severity="error"),r.kind==="view-update"?r.detail=[`${s} commits, ${t.length?`one per ${me(t)}`:"back to back"}`,`avg ${a}ms \xB7 wall ${i}ms`,u??""].filter(Boolean).join(" \xB7 "):r.kind==="state-update"&&u&&n.detail?r.detail=`${s} updates in a row \xB7 wall ${i}ms \xB7 first ${u} \xB7 last ${n.detail}`:r.detail=[`${s} in a row \xB7 wall ${i}ms`,u??""].filter(Boolean).join(" \xB7 ")}B(ne,"ft"),g(ne,"collapseRepeat");const J=[],_=new Map,oe=new Set;for(let e=0;e<h.length;e++){if(oe.has(e))continue;const t=h[e];if(!c(t)){J.push(t);continue}const r=[e];for(let u=e+1;u<h.length;u++){const l=h[u];if(l.group==="browser"){if(!c(l)||l.commitId!==t.commitId)break;r.push(u)}}for(const u of r)oe.add(u);const n=r.map(u=>h[u]),s=n.some(u=>u.kind!=="effect-run")?te(n):re(n);for(const u of n)u.id!==s.id&&_.set(u.id,s.id);J.push(s)}if(_.size>0||J.some(e=>e.kind==="view-update"||e.kind==="commit-effects")){h=J;for(const e of h)for(;e.parent&&_.has(e.parent);)e.parent=_.get(e.parent)}{const e=new Map;for(const t of h)t.kind==="view-update"&&t.commitId&&!e.has(t.commitId)&&e.set(t.commitId,t);for(const t of h){if(t.kind!=="commit-effects"||t.parent||!t.commitId)continue;const r=e.get(t.commitId);r&&r.start<=t.start&&(t.parent=r.id)}}{const e=new Set(["state-update","fetch","fetch-start","fetch-done","fetch-error","flow-origin"]),t=h.map((a,i)=>a.group==="browser"?i:-1).filter(a=>a>=0),r=g(a=>`${a.kind}|${a.sig??a.title}`,"keyOf"),n=t.map(a=>r(h[a])),s=g((a,i,f)=>{for(let k=0;k<f;k++)if(n[a+k]!==n[i+k])return!1;return!0},"sameBlock"),u=g(a=>n[a].split("|")[0],"kindAt"),l=g(a=>{let i=0,f=1;for(let k=1;k<=4&&a+k<=n.length;k++){let j=1;for(;a+(j+1)*k<=n.length&&s(a,a+j*k,k);)j+=1;j>=3&&k*j>i*f&&(i=k,f=j)}return{k:i,r:f}},"bestBlockAt"),$=new Map;for(let a=0;a<n.length;){const{k:i,r:f}=l(a);if(i===0){a+=1;continue}const k=[...Array(i).keys()].find(I=>e.has(u(a+I)));if(k!==void 0&&k>0){const I=l(a+k);if(I.k===i&&I.r>=3&&I.r>=f-1){a+=k;continue}}const j=n.slice(a,a+i).map(I=>I.split("|")[0]).filter(I=>e.has(I));for(let I=0;I<i;I++){const L=[];for(let N=0;N<f;N++)L.push(h[t[a+I+N*i]]);ne(L,j);for(const N of L.slice(1))$.set(N.id,L[0].id)}a+=i*f}if($.size>0){h=h.filter(a=>!$.has(a.id));for(const a of h)for(;a.parent&&$.has(a.parent);)a.parent=$.get(a.parent)}}{const e=new Map(h.map(t=>[t.id,t]));for(const t of h){const r=new Set([t.id]);let n=t;for(;n?.parent;){if(r.has(n.parent)){n.parent=void 0;break}r.add(n.parent),n=e.get(n.parent)}}}const se=g(e=>{const t=new Map;for(const n of h){if(!n.sig||p(n)!==e||n.kind==="server-request"||n.kind==="gateway-request"||n.group==="browser")continue;const s=`${n.parent??"root"}|${n.kind}|${n.sig}`,u=t.get(s);u?u.push(n):t.set(s,[n])}const r=new Map;for(const n of t.values()){if(n.length<3)continue;n.sort((i,f)=>i.start-f.start);const s=n[0];let u=!1;for(let i=0;i+1<n.length;i++)if(n[i+1].start<n[i].start+(n[i].durationMs??0)){u=!0;break}const l=s.start,$=Math.max(...n.map(i=>i.start+(i.durationMs??0))),a=n.reduce((i,f)=>i+(f.durationMs??0),0);s.count=n.length,s.parallel=u,s.avgMs=Math.round(a/n.length*10)/10,s.wallMs=$-l,s.durationMs=$-l,s.title=`\xD7${n.length} ${s.title.replace(/\s*\((?:\d+ rows?, )?\d+ms\)$/,"")}`,s.detail=`${s.sig} \xD7${n.length} ${u?"\u2016 parallel":"\u21BB sequential loop"} \xB7 avg ${s.avgMs}ms \xB7 wall ${s.wallMs}ms`,s.items=n.slice(0,12).map(i=>({start:i.start-l,...i.durationMs!=null?{durationMs:i.durationMs}:{}})),s.memberIds=n.flatMap(i=>i.memberIds??[i.id]),n.some(i=>i.severity==="error")&&(s.severity="error");for(const i of n.slice(1))r.set(i.id,s.id)}if(r.size!==0){h=h.filter(n=>!r.has(n.id));for(const n of h)for(;n.parent&&r.has(n.parent);)n.parent=r.get(n.parent)}},"aggregatePass");se(!0),se(!1);for(const e of h){if(!p(e))continue;const t=e.durationMs??0;if(t<40)continue;const r=h.filter($=>$.parent===e.id&&($.durationMs??0)>0).map($=>[$.start,$.start+($.durationMs??0)]).sort(($,a)=>$[0]-a[0]);let n=0,s=-1,u=-1;for(const[$,a]of r)$>u?(u>s&&(n+=u-s),s=$,u=a):a>u&&(u=a);u>s&&(n+=u-s);const l=Math.max(0,t-n);(l>=50||l/t>=.7&&t>=40)&&(e.selfMs=l,e.detail=`\u26A0 cpu-heavy: ${l}ms self-time of ${t}ms${e.detail?" \xB7 "+e.detail:""}`)}return{events:h}}B(buildFlowDetail,"buildFlowDetail"),g(buildFlowDetail,"buildFlowDetail");
|
|
1
|
+
"use strict";var ge=Object.defineProperty;var A=(u,q)=>ge(u,"name",{value:q,configurable:!0});var $e=Object.defineProperty,g=A((u,q)=>$e(u,"name",{value:q,configurable:!0}),"h");import{redactDeep as ye,redactText as ue}from"./redact.js";import{boundedClone as ke}from"@lensmcp/protocol-types";export function areaOf(u){const{source:q,category:n,title:M,kind:y}=u;return q==="ai"?"ai":y?.startsWith("memory")||y==="db-query"?"data":q==="gateway"||y==="gateway-request"?"gateway":y==="queue-process"||y==="queue-active"||y==="queue-completed"||y==="queue-failed"?"services":q==="db"||q==="redis"||q==="queue"||y==="redis-op"||y?.startsWith("queue-")?"data":q==="external"||y==="egress"?"network":q==="nestjs"||q==="nest"?/(?:Repository|Repo|Dao|Store)\./.test(M)?"data":"services":q==="valtio"?"state":n==="network"?"network":"browser"}A(areaOf,"areaOf"),g(areaOf,"areaOf");const de=240,we=new Set(["systemPrompt","prompt","prompts","messages","completion","completions","input","output","draft","text","content","result","arguments","params"]),qe=new Set(["tenantId","caller","userId","accountId","email","subject"]),Me=8;function ce(u){const q=Y(u,0);return ke(ye({redacted:!0,...q}),{rawByteLimit:16*1024}).value}A(ce,"yt"),g(ce,"aiProjection");function Y(u,q){if(typeof u=="string")return le(u);if(u===null||typeof u!="object")return u;if(q>=Me)return;if(Array.isArray(u))return u.slice(0,32).map(M=>Y(M,q+1));const n={};for(const[M,y]of Object.entries(u))if(!qe.has(M)){if(we.has(M)){typeof y=="string"?n[`${M}Chars`]=y.length:Array.isArray(y)?n[`${M}Count`]=y.length:y!=null&&(n[`${M}Present`]=!0);continue}n[M]=Y(y,q+1)}return n}A(Y,"tt"),g(Y,"projectAi");function le(u){return u.length<=de?ue(u):`${ue(u.slice(0,de))}\u2026`}A(le,"kt"),g(le,"clipText");export function trimFlowEvent(u,q={}){const n=u.raw??{},M=n.render??{},y=n.span??{},W=n.request??{},f=g(d=>typeof d=="number"?d:void 0,"num"),b=n.loop??{},T=n.effect??{},P=f(n.durationMs)??f(y.durationMs)??f(W.durationMs)??f(b.durationMs)??f(M.actualDurationMs)??f(T.durationMs),p=typeof n.kind=="string"?n.kind:void 0,_=p==="fetch-done"||p==="fetch-error",K=f(M.commitTime),R=p==="render"&&K!==void 0&&Math.abs(K-u.timestamp)<6e4,G=_?u.timestamp:R?K:f(n.startedAt)??f(y.startTime)??f(W.startedAt)??f(b.startedAt)??(P!==void 0?u.timestamp-P:u.timestamp);let j;if(typeof n.query=="string"){const d=Array.isArray(n.params)?` [${n.params.join(", ")}]`:"",w=typeof n.result=="string"?` \u2192 ${n.result}`:"";j=n.query+d+w}else if(typeof n.command=="string"){const d=typeof n.value=="string"?` \u2192 ${n.value}`:n.hit===!1?" \u2192 (miss)":"";j=n.command+d}else if(p==="state-update"){const d=n.update??{};let w="";try{w=JSON.stringify(d.preview)}catch{}j=`${d.storeId??""}.${d.path??""} = ${w}`}else p==="queue-completed"&&typeof n.returned=="string"?j=`returned ${n.returned}`:p==="queue-enqueued"&&(j=`${n.job??"job"}#${n.jobId??"?"} \u2192 ${n.queue??""}${typeof n.data=="string"?" "+n.data:""}`);let F;if(p==="gateway-request")F=`${n.host??""}${String(n.url??"").split("?")[0]}`;else if(p==="fetch-start"||p==="fetch-done"||p==="fetch-error")try{const d=new URL(String(n.href??n.url??""));F=`${d.hostname}${d.pathname}`}catch{}let C;if(p==="gateway-request"&&Array.isArray(n.steps))C=n.steps.map(d=>{const w=[String(d.step)];return d.project&&w.push(String(d.project)),d.prefix&&w.push(String(d.prefix)),d.mode&&w.push(String(d.mode)+(d.ok===!1?" \u2717":d.ok===!0?" \u2713":"")),d.caller&&w.push("by "+String(d.caller)),d.pod&&w.push(String(d.pod)),typeof d.atMs=="number"&&w.push(`@${d.atMs}ms`),w.join(" ")}),j=C.map(d=>d.replace(/ @\d+ms$/,"")).join(" \u2192 ");else if(p==="egress")j=`${n.method??"GET"} ${n.host??""} \u2192 ${n.status??"?"}`;else if(u.source==="ai"){if(p==="ai-call"){const d=n.usage??{},w=d.promptTokens!==void 0||d.completionTokens!==void 0?` \xB7 ${f(d.promptTokens)??"?"}\u2192${f(d.completionTokens)??"?"} tok`:"",S=n.status!==void 0?` \xB7 ${String(n.status)}`:n.error?" \xB7 failed":"",z=f(n.toolCallCount)?` \xB7 ${f(n.toolCallCount)} tool call${f(n.toolCallCount)===1?"":"s"}`:"",L=f(n.durationMs)!==void 0?` \xB7 ${(f(n.durationMs)/1e3).toFixed(1)}s`:"";j=`${n.provider??""}/${n.model??"?"}${w}${S}${z}${L}${n.stream===!0?" \xB7 stream":""}`}else if(p==="ai-round"){const d=n.model??{},w=n.usage??{},S=Array.isArray(n.toolCalls)?n.toolCalls:[],z=`${f(w.promptTokens)??"?"}\u2192${f(w.completionTokens)??"?"} tok`,L=S.length?` \xB7 ${S.length} tool call${S.length===1?"":"s"}`:"",V=f(n.durationMs)!==void 0?` \xB7 ${(f(n.durationMs)/1e3).toFixed(1)}s`:"";j=`${d.provider??""}/${d.model??""} \xB7 ${z}${L}${V}`}else if(p==="ai-turn-start"){const d=Array.isArray(n.tools)?n.tools:[];j=`skill ${n.skill??""} \xB7 ${n.output??""}${d.length?" \xB7 tools: "+d.join(", "):""}`}else if(p==="ai-tool-call"){const d=n.call??{},w=n.result??{},S=f(n.durationMs)!==void 0?` \xB7 ${(f(n.durationMs)/1e3).toFixed(1)}s`:"";j=`${d.name??"tool"} ${w.error!=null?"\u2717 "+String(w.error):"\u2713"}${S}`}else if(p==="ai-turn-end"){const d=n.usage??{},w=f(n.rounds)!==void 0?` \xB7 ${f(n.rounds)} round${f(n.rounds)===1?"":"s"}`:"",S=f(n.durationMs)!==void 0?` \xB7 ${(f(n.durationMs)/1e3).toFixed(1)}s`:"";j=`${n.ok===!1?"FAILED":"ok"} \xB7 ${f(d.promptTokens)??"?"}+${f(d.completionTokens)??"?"} tok${w}${S}`}}let B;p==="db-query"&&typeof n.query=="string"?B=n.query:p==="redis-op"?B=`${n.op??""} ${String(n.key??"").replace(/\d+/g,"#")}`:p==="egress"?B=`${n.method??"GET"} ${n.host??""}`:p==="ai-call"?B=`ai ${n.provider??""} ${n.model??""} ${n.api??""}`:p==="queue-enqueued"?B=`enqueue ${n.queue??""}`:p==="span"&&typeof y.name=="string"?B=y.name:p==="memory-mutation"&&(B=u.title);const D=u.context??{},H=M.commitId??T.commitId??n.commitId,h=Array.isArray(M.why)?M.why.map(d=>typeof d?.type=="string"?d.type:void 0).filter(d=>!!d):[];return{id:u.id,t:u.timestamp,start:G,...B?{sig:B}:{},...F?{urlKey:F}:{},...typeof n.queue=="string"?{queue:n.queue}:{},...n.jobId!=null?{jobId:String(n.jobId)}:{},source:u.source,category:u.category,severity:u.severity,title:u.title,kind:p,area:areaOf({source:u.source,category:u.category,title:u.title,kind:p}),...typeof y.role=="string"?{role:y.role}:{},...typeof D.requestId=="string"?{requestId:D.requestId}:{},...typeof D.parentRequestId=="string"?{parentRequestId:D.parentRequestId}:{},...typeof H=="string"?{commitId:H}:{},...typeof M.componentName=="string"?{component:M.componentName}:{},...typeof M.phase=="string"?{phase:M.phase}:{},...h.length?{why:h}:{},...p==="effect-run"&&typeof T.hookLogicalId=="string"?{hook:T.hookLogicalId}:{},...p==="effect-run"&&T.cleanupRan===!0?{cleanup:!0}:{},...p==="render-commit-truncated"?{renderTotal:f(n.total),renderPublished:f(n.published)}:{},...typeof D.projectName=="string"?{project:D.projectName}:typeof n.project=="string"?{project:n.project}:{},...P!==void 0?{durationMs:P}:{},..._&&f(n.startedAt)!==void 0?{fetchStartedAt:f(n.startedAt)}:{},...u.source==="ai"?{ai:q.includeRaw?n:ce(n)}:{},...j?{detail:j}:{},...C?{steps:C}:{}}}A(trimFlowEvent,"trimFlowEvent"),g(trimFlowEvent,"trimFlowEvent");const fe=1;function pe(u){return u==="flow-origin"?0:u?.endsWith("-start")?1:u==="gateway-request"?2:u==="server-request"?3:u==="span"?4:u==="render"||u==="render-commit-truncated"?6:u==="effect-run"?7:5}A(pe,"wt"),g(pe,"kindRank");function E(u){return u.kind==="span"&&u.role==="controller"?3.5:u.kind==="queue-process"?3:pe(u.kind)}A(E,"J"),g(E,"causalRank");export function buildFlowDetail(u,q={}){const n=u.slice().filter(e=>{const t=e.raw??{},r=typeof t.kind=="string"?t.kind:"";if(r==="subscription-added"||r==="subscription-removed"||r==="hook-run"||r==="store-created"||r==="singleton-instance")return!1;const o=t.span;return!(typeof o?.name=="string"&&o.name.startsWith("nest.request "))}).map(e=>trimFlowEvent(e,q)),M=new Set;for(let e=n.length-1;e>=0;e--)M.has(n[e].id)?n.splice(e,1):M.add(n[e].id);const y=new Map;for(const e of n)(e.kind==="gateway-request"||e.kind==="queue-process")&&e.requestId&&!y.has(e.requestId)&&y.set(e.requestId,e.start);const W=g(e=>e.source!=="react"&&e.source!=="valtio"&&e.requestId&&y.has(e.requestId)?y.get(e.requestId):e.start,"anchorOf");n.sort((e,t)=>W(e)-W(t)||e.start-t.start||E(e)-E(t)||(t.durationMs??0)-(e.durationMs??0)||e.t-t.t);const f=g(e=>(e.kind==="span"||e.kind==="server-request"||e.kind==="gateway-request"||e.kind==="queue-process")&&(e.durationMs??0)>0,"spanLike"),b=n;for(const e of b)e.group=e.source==="react"||e.source==="valtio"||e.source==="client-runtime"||e.source==="visual"?"browser":"server";const T=new Map;for(const e of b)if((e.kind==="gateway-request"||e.kind==="queue-process")&&e.requestId&&e.project){const t=T.get(e.requestId)??[];t.push({project:e.project,start:e.start,end:e.start+(e.durationMs??0)}),T.set(e.requestId,t)}const P=g((e,t)=>{const r=T.get(e);if(r?.length)return r.every(o=>o.project===r[0].project)?r[0].project:r.filter(o=>o.start<=t+1&&o.end>=t-1).sort((o,s)=>s.start-o.start)[0]?.project},"projectOf");for(const e of b)if(!e.project&&e.source==="nestjs"&&e.requestId){const t=P(e.requestId,e.start);t&&(e.project=t)}for(const e of b){let t;const r=e.requestId;for(const o of b){if(o===e||!f(o)||o.group!==e.group||o.requestId!==r)continue;const s=o.project,c=e.project;if(o.kind!=="gateway-request"&&e.kind!=="gateway-request"&&s!==void 0&&c!==void 0&&s!==c)continue;const m=o.durationMs??0,$=e.durationMs??0,a=Math.abs(o.start-e.start)<=fe,i=E(o),l=E(e),k=o.start<=e.start;if(!(o.start+m>e.start&&(k||a&&i<l)&&(!a||(i!==l?i<l:k&&m>$))))continue;const x=t?E(t):-1,I=t?Math.abs(o.start-t.start)<=fe:!1;(!t||!I&&o.start>t.start||I&&i>x||I&&i===x&&(o.durationMs??0)<(t.durationMs??0))&&(t=o)}t&&(e.parent=t.id)}const p=new Map;for(const e of b){if(!e.requestId)continue;const t=p.get(e.requestId);t?t.push(e):p.set(e.requestId,[e])}const _=new Map(b.map(e=>[e.id,e])),K=g((e,t)=>{const r=(p.get(e)??[]).filter(o=>f(o)&&o.start<=t&&o.start+(o.durationMs??0)>=t);if(r.length!==0)return r.sort((o,s)=>s.start-o.start||(o.durationMs??0)-(s.durationMs??0)),r[0]},"callerNodeFor");for(const e of b){if(!e.parentRequestId||e.kind!=="server-request"&&e.kind!=="gateway-request")continue;const t=e.parent?_.get(e.parent):void 0;if(t&&t.requestId===e.requestId)continue;const r=K(e.parentRequestId,e.start);r&&r!==e&&r.start<=e.start&&(e.parent=r.id)}const R=b,G=new Map;for(const e of R)e.kind==="gateway-request"&&e.requestId&&!G.has(e.requestId)&&G.set(e.requestId,e);for(const e of R)if(e.group==="server"&&!e.parent&&e.requestId&&e.kind!=="gateway-request"){const t=G.get(e.requestId);t&&t!==e&&t.start<=e.start&&(e.parent=t.id)}const j=new Set,F=new Set,C=[];for(const e of R){if(e.kind!=="gateway-request")continue;if(!e.requestId){C.push(e);continue}const t=p.get(e.requestId)??[];if(t.filter(s=>s.kind==="gateway-request").sort((s,c)=>s.start-c.start)[0]!==e)continue;const r=t.find(s=>s.kind==="fetch-start"),o=t.find(s=>s.kind==="fetch-done"||s.kind==="fetch-error");if(!r&&!o){C.push(e);continue}r&&(e.fetchStartId=r.id,j.add(r.id)),o&&(e.fetchDoneId=o.id,F.add(o.id))}const B=new Map;for(const e of R){if(!e.urlKey)continue;const t=B.get(e.urlKey)??{gws:[],starts:[],dones:[]};e.kind==="gateway-request"?C.includes(e)&&t.gws.push(e):e.kind==="fetch-start"?j.has(e.id)||t.starts.push(e):(e.kind==="fetch-done"||e.kind==="fetch-error")&&(F.has(e.id)||t.dones.push(e)),B.set(e.urlKey,t)}for(const{gws:e,starts:t,dones:r}of B.values()){e.sort((s,c)=>s.start-c.start),t.sort((s,c)=>s.start-c.start);const o=new Set;e.forEach((s,c)=>{const m=t[c];m&&(s.fetchStartId=m.id);const $=m?m.t:s.start;let a,i=1/0;for(const l of r){if(o.has(l.id))continue;const k=l.fetchStartedAt??l.start-(l.durationMs??0),x=Math.abs(k-$);x<i&&(i=x,a=l)}a&&(s.fetchDoneId=a.id,o.add(a.id))})}const D=new Map,H=new Map;for(const e of R)!e.queue||!e.jobId||(e.kind==="queue-enqueued"&&D.set(`${e.queue}#${e.jobId}`,e),e.kind==="queue-process"&&H.set(`${e.queue}#${e.jobId}`,e));for(const e of R){if(!e.queue||!e.jobId)continue;const t=`${e.queue}#${e.jobId}`;if(e.kind==="queue-process"&&!e.parent){const r=D.get(t);r&&r!==e&&r.start<=e.start&&(e.parent=r.id)}else if(e.kind==="queue-active"||e.kind==="queue-completed"||e.kind==="queue-failed"){const r=H.get(t);r&&r!==e&&r.start<=e.start&&(e.parent=r.id)}}let h=b;{const e=new Map;for(const r of h)r.kind==="fetch-start"&&e.set(r.id,r);const t=new Map;for(const r of h){if(r.kind!=="fetch-done"&&r.kind!=="fetch-error")continue;const o=[...e.values()].find(m=>m.requestId&&m.requestId===r.requestId)?.id,s=o?e.get(o):void 0;if(!s)continue;const c=r.fetchStartedAt??s.start;s.title=r.title,s.severity=r.severity,s.durationMs=r.durationMs??Math.max(0,r.t-c),s.kind="fetch",r.detail&&!s.detail&&(s.detail=r.detail),t.set(r.id,s.id)}if(t.size>0){h=h.filter(r=>!t.has(r.id));for(const r of h)for(r.fetchDoneId&&t.has(r.fetchDoneId)&&(r.fetchDoneId=t.get(r.fetchDoneId));r.parent&&t.has(r.parent);)r.parent=t.get(r.parent)}}const d=g(e=>!!e.commitId&&(e.kind==="render"||e.kind==="effect-run"||e.kind==="render-commit-truncated"),"commitMember"),w=g(e=>[...new Set(e)],"uniq"),S=g(e=>w(e.map(t=>t.component).filter(t=>!!t)),"named"),z=g(e=>e.length<=2?e.join(" + "):`${e[0]} + ${e.length-1} more`,"nameList"),L=g(e=>{const t=new Map;for(const r of e)r.component&&t.set(r.component,(t.get(r.component)??0)+1);return[...t].map(([r,o])=>o>1?`${r} \xD7${o}`:r).join(", ")},"tally"),V=g(e=>{if(!e)return"effect";const t=/([^/\\]+?):(\d+)(?::\d+)?$/.exec(e);return t?`${t[1]}:${t[2]}`:e},"shortHook"),X=g(e=>`${V(e.hook)}${e.cleanup?" (cleanup)":""}`,"hookLabel"),Z=g(e=>{const t=Math.min(...e.map(o=>o.start)),r=Math.max(...e.map(o=>o.start+(o.durationMs??0)));return{start:t,durationMs:r-t}},"extent"),ee=g(e=>(delete e.component,delete e.why,delete e.phase,delete e.hook,delete e.cleanup,delete e.renderTotal,delete e.renderPublished,e),"asGroup");function te(e){const t=e[0],r=e.filter(v=>v.kind==="render"),o=e.filter(v=>v.kind==="effect-run"),s=e.find(v=>v.kind==="render-commit-truncated"),c=[...r].sort((v,U)=>(U.durationMs??0)-(v.durationMs??0)),m=c.filter(v=>(v.why??[]).some(U=>U!=="parent-render")),$=w(m.flatMap(v=>(v.why??[]).filter(U=>U!=="parent-render"))),a=S(m),i=r.filter(v=>v.phase==="mount"),l=r.filter(v=>v.phase!=="mount"&&!m.includes(v)),k=c[0]?.component??"view",x=r.length>0&&i.length===r.length,I=$.length?$.join(" + "):void 0,N=x?"mount":"update",O=a.length?`view update \xB7 ${z(a)} \u2190 ${I}`:x?`view mount \xB7 ${k}`:`view update \xB7 ${k}`,ae=s?.renderTotal??r.length,he=[i.length?`mounted ${L(i)}`:"",l.length?`re-rendered ${L(l)}`:"",!a.length&&!x&&r.length?"trigger not recorded \u2014 every captured render came from its parent":"",s?`${ae} renders, ${s.renderPublished??r.length} recorded (per-commit cap)`:`${r.length} render${r.length===1?"":"s"}`,o.length?`${o.length} effect${o.length===1?"":"s"}: ${o.map(X).join(", ")}`:""].filter(Boolean).join(" \xB7 "),ie=S(r);return ee({...t,...Z(e),kind:"view-update",title:O,detail:he,commitId:t.commitId,viewPhase:N,...I?{cause:I}:{},causeComponents:a,components:ie,mounted:S(i),renderCount:ae,recordedRenders:r.length,effects:o.length,effectHooks:o.map(X),memberIds:e.map(v=>v.id),sig:`commit:${N}:${I??"-"}:${a.join(",")}:${ie.slice().sort().join(",")}`,severity:e.some(v=>v.severity==="error")?"error":t.severity})}A(te,"ct"),g(te,"viewUpdateNode");function re(e){const t=e[0],r=e.map(X);return ee({...t,...Z(e),kind:"commit-effects",title:e.length===1?`effect \xB7 ${r[0]}`:`effects \xB7 ${r[0]} + ${e.length-1} more`,detail:r.join(", "),commitId:t.commitId,effects:e.length,effectHooks:r,memberIds:e.map(o=>o.id),sig:`effects:${r.slice().sort().join(",")}`})}A(re,"dt"),g(re,"effectsNode");const me=g(e=>w(e.map(t=>t==="state-update"?"state update":t==="fetch-start"?"request":t==="flow-origin"?"interaction":"response")).join(" + "),"describeInputs");function oe(e,t){const r=e[0],o=e[e.length-1],s=e.length,c=r.detail,m=Math.min(...e.map(l=>l.start)),$=Math.max(...e.map(l=>l.start+(l.durationMs??0))),a=Math.round(e.reduce((l,k)=>l+(k.durationMs??0),0)/s*10)/10,i=Math.round(($-m)*10)/10;r.count=s,r.avgMs=a,r.wallMs=i,r.durationMs=$-m,r.items=e.slice(0,12).map(l=>({start:l.start-m,...l.durationMs!=null?{durationMs:l.durationMs}:{}})),r.memberIds=e.flatMap(l=>l.memberIds??[l.id]),r.title=`\xD7${s} ${r.title}`,e.some(l=>l.severity==="error")&&(r.severity="error"),r.kind==="view-update"?r.detail=[`${s} commits, ${t.length?`one per ${me(t)}`:"back to back"}`,`avg ${a}ms \xB7 wall ${i}ms`,c??""].filter(Boolean).join(" \xB7 "):r.kind==="state-update"&&c&&o.detail?r.detail=`${s} updates in a row \xB7 wall ${i}ms \xB7 first ${c} \xB7 last ${o.detail}`:r.detail=[`${s} in a row \xB7 wall ${i}ms`,c??""].filter(Boolean).join(" \xB7 ")}A(oe,"ft"),g(oe,"collapseRepeat");const J=[],Q=new Map,ne=new Set;for(let e=0;e<h.length;e++){if(ne.has(e))continue;const t=h[e];if(!d(t)){J.push(t);continue}const r=[e];for(let c=e+1;c<h.length;c++){const m=h[c];if(m.group==="browser"){if(!d(m)||m.commitId!==t.commitId)break;r.push(c)}}for(const c of r)ne.add(c);const o=r.map(c=>h[c]),s=o.some(c=>c.kind!=="effect-run")?te(o):re(o);for(const c of o)c.id!==s.id&&Q.set(c.id,s.id);J.push(s)}if(Q.size>0||J.some(e=>e.kind==="view-update"||e.kind==="commit-effects")){h=J;for(const e of h)for(;e.parent&&Q.has(e.parent);)e.parent=Q.get(e.parent)}{const e=new Map;for(const t of h)t.kind==="view-update"&&t.commitId&&!e.has(t.commitId)&&e.set(t.commitId,t);for(const t of h){if(t.kind!=="commit-effects"||t.parent||!t.commitId)continue;const r=e.get(t.commitId);r&&r.start<=t.start&&(t.parent=r.id)}}{const e=new Set(["state-update","fetch","fetch-start","fetch-done","fetch-error","flow-origin"]),t=h.map((a,i)=>a.group==="browser"?i:-1).filter(a=>a>=0),r=g(a=>`${a.kind}|${a.sig??a.title}`,"keyOf"),o=t.map(a=>r(h[a])),s=g((a,i,l)=>{for(let k=0;k<l;k++)if(o[a+k]!==o[i+k])return!1;return!0},"sameBlock"),c=g(a=>o[a].split("|")[0],"kindAt"),m=g(a=>{let i=0,l=1;for(let k=1;k<=4&&a+k<=o.length;k++){let x=1;for(;a+(x+1)*k<=o.length&&s(a,a+x*k,k);)x+=1;x>=3&&k*x>i*l&&(i=k,l=x)}return{k:i,r:l}},"bestBlockAt"),$=new Map;for(let a=0;a<o.length;){const{k:i,r:l}=m(a);if(i===0){a+=1;continue}const k=[...Array(i).keys()].find(I=>e.has(c(a+I)));if(k!==void 0&&k>0){const I=m(a+k);if(I.k===i&&I.r>=3&&I.r>=l-1){a+=k;continue}}const x=o.slice(a,a+i).map(I=>I.split("|")[0]).filter(I=>e.has(I));for(let I=0;I<i;I++){const N=[];for(let O=0;O<l;O++)N.push(h[t[a+I+O*i]]);oe(N,x);for(const O of N.slice(1))$.set(O.id,N[0].id)}a+=i*l}if($.size>0){h=h.filter(a=>!$.has(a.id));for(const a of h)for(;a.parent&&$.has(a.parent);)a.parent=$.get(a.parent)}}{const e=new Map(h.map(t=>[t.id,t]));for(const t of h){const r=new Set([t.id]);let o=t;for(;o?.parent;){if(r.has(o.parent)){o.parent=void 0;break}r.add(o.parent),o=e.get(o.parent)}}}const se=g(e=>{const t=new Map;for(const o of h){if(!o.sig||f(o)!==e||o.kind==="server-request"||o.kind==="gateway-request"||o.group==="browser")continue;const s=`${o.parent??"root"}|${o.kind}|${o.sig}`,c=t.get(s);c?c.push(o):t.set(s,[o])}const r=new Map;for(const o of t.values()){if(o.length<3)continue;o.sort((i,l)=>i.start-l.start);const s=o[0];let c=!1;for(let i=0;i+1<o.length;i++)if(o[i+1].start<o[i].start+(o[i].durationMs??0)){c=!0;break}const m=s.start,$=Math.max(...o.map(i=>i.start+(i.durationMs??0))),a=o.reduce((i,l)=>i+(l.durationMs??0),0);s.count=o.length,s.parallel=c,s.avgMs=Math.round(a/o.length*10)/10,s.wallMs=$-m,s.durationMs=$-m,s.title=`\xD7${o.length} ${s.title.replace(/\s*\((?:\d+ rows?, )?\d+ms\)$/,"")}`,s.detail=`${s.sig} \xD7${o.length} ${c?"\u2016 parallel":"\u21BB sequential loop"} \xB7 avg ${s.avgMs}ms \xB7 wall ${s.wallMs}ms`,s.items=o.slice(0,12).map(i=>({start:i.start-m,...i.durationMs!=null?{durationMs:i.durationMs}:{}})),s.memberIds=o.flatMap(i=>i.memberIds??[i.id]),o.some(i=>i.severity==="error")&&(s.severity="error");for(const i of o.slice(1))r.set(i.id,s.id)}if(r.size!==0){h=h.filter(o=>!r.has(o.id));for(const o of h)for(;o.parent&&r.has(o.parent);)o.parent=r.get(o.parent)}},"aggregatePass");se(!0),se(!1);for(const e of h){if(!f(e))continue;const t=e.durationMs??0;if(t<40)continue;const r=h.filter($=>$.parent===e.id&&($.durationMs??0)>0).map($=>[$.start,$.start+($.durationMs??0)]).sort(($,a)=>$[0]-a[0]);let o=0,s=-1,c=-1;for(const[$,a]of r)$>c?(c>s&&(o+=c-s),s=$,c=a):a>c&&(c=a);c>s&&(o+=c-s);const m=Math.max(0,t-o);(m>=50||m/t>=.7&&t>=40)&&(e.selfMs=m,e.detail=`\u26A0 cpu-heavy: ${m}ms self-time of ${t}ms${e.detail?" \xB7 "+e.detail:""}`)}return{events:h}}A(buildFlowDetail,"buildFlowDetail"),g(buildFlowDetail,"buildFlowDetail");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nx.json#lensmcp.flows` — what a workspace says about how its flows should be cut and named.
|
|
3
|
+
*
|
|
4
|
+
* {
|
|
5
|
+
* "lensmcp": {
|
|
6
|
+
* "flows": {
|
|
7
|
+
* "categories": [
|
|
8
|
+
* { "category": "checkout", "route": "POST /api/checkout" },
|
|
9
|
+
* { "category": "auth", "route": "/session/*" },
|
|
10
|
+
* { "category": "cart", "origin": "ui:web/pages/cart.tsx:*" }
|
|
11
|
+
* ],
|
|
12
|
+
* "segments": { "phaseGapMs": 2000, "maxSegmentEvents": 300 }
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* The file is edited by hand, so the parser is forgiving in the one way that matters: ONE bad entry
|
|
18
|
+
* never costs the good ones. Each invalid rule is dropped and named in `problems` (which the server
|
|
19
|
+
* logs at boot), and an out-of-range number falls back to its default rather than to nonsense. It
|
|
20
|
+
* never throws.
|
|
21
|
+
*
|
|
22
|
+
* Pure: takes the already-parsed JSON value.
|
|
23
|
+
*/
|
|
24
|
+
import type { BaseEvent } from '@lensmcp/protocol-types';
|
|
25
|
+
import { type CategoryRule, type FlowClassification } from './flow-categories.js';
|
|
26
|
+
import { type FlowSegmentation, type SegmentOptions } from './flow-segments.js';
|
|
27
|
+
export interface FlowsConfig {
|
|
28
|
+
categories: readonly CategoryRule[];
|
|
29
|
+
segments: SegmentOptions;
|
|
30
|
+
/** Everything that was wrong with the input, one line each. Empty when the config was clean. */
|
|
31
|
+
problems: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
/** No rules, default cuts. */
|
|
34
|
+
export declare const DEFAULT_FLOWS_CONFIG: FlowsConfig;
|
|
35
|
+
/** Parse the value of `nx.json#lensmcp.flows`. Anything may be passed; it never throws. */
|
|
36
|
+
export declare function parseFlowsConfig(input: unknown): FlowsConfig;
|
|
37
|
+
/** A flow, cut and named: everything the dashboard and the MCP tools show about one flow. */
|
|
38
|
+
export interface FlowAnalysis {
|
|
39
|
+
segmentation: FlowSegmentation;
|
|
40
|
+
classification: FlowClassification;
|
|
41
|
+
/** segment id → its category (an `api` request, a `ui` burst, or one of your route/project rules). */
|
|
42
|
+
segmentCategories: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
/** Segment a flow, classify it, and categorise each segment — with the workspace's own config. */
|
|
45
|
+
export declare function analyzeFlow(events: readonly BaseEvent[], config?: Pick<FlowsConfig, 'categories' | 'segments'>): FlowAnalysis;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var b=Object.defineProperty;var c=(s,e)=>b(s,"name",{value:e,configurable:!0});var y=Object.defineProperty,l=c((s,e)=>y(s,"name",{value:e,configurable:!0}),"u");import{classifyFlow as $,categoryOfSegment as h}from"./flow-categories.js";import{DEFAULT_SEGMENT_OPTIONS as w,segmentFlow as j}from"./flow-segments.js";export const DEFAULT_FLOWS_CONFIG={categories:[],segments:{},problems:[]};const p=200,f=200,m=64,F=/^[A-Za-z0-9][A-Za-z0-9 ._:/-]*$/,O=["category","flowId","originType","origin","route","project"],A={phaseGapMs:[50,6e5],burstGapMs:[10,6e4],attachWindowMs:[0,6e4],maxSegmentEvents:[20,1e4],occurrenceSlackMs:[0,1e3]},g=l(s=>typeof s=="object"&&s!==null&&!Array.isArray(s),"isObj");function d(s,e){if(!g(s))return{problems:[`${e}: not an object`]};const o=[];for(const t of Object.keys(s))O.includes(t)||o.push(`${e}: unknown field "${t}" ignored`);const n=s.category;if(typeof n!="string"||n.trim()==="")return{problems:[...o,`${e}: "category" must be a non-empty string \u2014 rule dropped`]};const i=n.trim();if(i.length>m||!F.test(i))return{problems:[...o,`${e}: category "${i.slice(0,40)}" is not a usable name (letters, digits, space . _ : / -; up to ${m}) \u2014 rule dropped`]};const u={category:i};let a=0;for(const t of["flowId","originType","origin","route","project"]){const r=s[t];if(r!==void 0){if(typeof r!="string"||r===""||r.length>f)return{problems:[...o,`${e}: "${t}" must be a non-empty string of up to ${f} characters \u2014 rule dropped`]};u[t]=r,a+=1}}return a===0?{problems:[...o,`${e}: "${i}" has no match field (flowId, originType, origin, route or project) \u2014 rule dropped`]}:{rule:u,problems:o}}c(d,"w"),l(d,"parseRule");export function parseFlowsConfig(s){if(s==null)return DEFAULT_FLOWS_CONFIG;if(!g(s))return{...DEFAULT_FLOWS_CONFIG,problems:["lensmcp.flows must be an object \u2014 ignored"]};const e=[],o=[],n=s.categories;n!==void 0&&(Array.isArray(n)?(n.length>p&&e.push(`flows.categories has ${n.length} rules; only the first ${p} are used`),n.slice(0,p).forEach((a,t)=>{const r=d(a,`flows.categories[${t}]`);e.push(...r.problems),r.rule&&o.push(r.rule)})):e.push("flows.categories must be an array \u2014 ignored"));const i={},u=s.segments;if(u!==void 0)if(!g(u))e.push("flows.segments must be an object \u2014 ignored");else for(const[a,t]of Object.entries(u)){const r=A[a];if(!r){e.push(`flows.segments.${a}: unknown option ignored`);continue}if(typeof t!="number"||!Number.isFinite(t)||t<r[0]||t>r[1]){e.push(`flows.segments.${a}: ${JSON.stringify(t)} is outside ${r[0]}\u2013${r[1]}; using the default (${w[a]})`);continue}i[a]=t}return{categories:o,segments:i,problems:e}}c(parseFlowsConfig,"parseFlowsConfig"),l(parseFlowsConfig,"parseFlowsConfig");export function analyzeFlow(s,e=DEFAULT_FLOWS_CONFIG){const o=j(s,e.segments),n=$(s,o,e.categories),i={};for(const u of o.segments)i[u.id]=h(u,n,e.categories);return{segmentation:o,classification:n,segmentCategories:i}}c(analyzeFlow,"analyzeFlow"),l(analyzeFlow,"analyzeFlow");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -21,10 +21,15 @@
|
|
|
21
21
|
"types": "./lib/bus-rotation.d.ts",
|
|
22
22
|
"import": "./lib/bus-rotation.js",
|
|
23
23
|
"default": "./lib/bus-rotation.js"
|
|
24
|
+
},
|
|
25
|
+
"./ai-content": {
|
|
26
|
+
"types": "./lib/ai-content.d.ts",
|
|
27
|
+
"import": "./lib/ai-content.js",
|
|
28
|
+
"default": "./lib/ai-content.js"
|
|
24
29
|
}
|
|
25
30
|
},
|
|
26
31
|
"dependencies": {
|
|
27
|
-
"@lensmcp/protocol-types": "1.23.
|
|
32
|
+
"@lensmcp/protocol-types": "1.23.6",
|
|
28
33
|
"tslib": "^2.3.0"
|
|
29
34
|
},
|
|
30
35
|
"license": "Apache-2.0",
|