@lensmcp/core 1.24.2 → 1.24.4
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/answer-budget.d.ts +122 -0
- package/lib/answer-budget.js +2 -0
- package/lib/flow-categories.js +1 -1
- package/lib/flow-shape.d.ts +20 -2
- package/lib/flow-shape.js +1 -1
- package/lib/headless-flow.d.ts +45 -0
- package/lib/headless-flow.js +1 -0
- package/lib/patterns.d.ts +18 -1
- package/lib/patterns.js +1 -1
- package/package.json +7 -2
package/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './lib/ulid.js';
|
|
|
3
3
|
export * from './lib/event-bus.js';
|
|
4
4
|
export * from './lib/event-redactor.js';
|
|
5
5
|
export * from './lib/resource-store.js';
|
|
6
|
+
export * from './lib/answer-budget.js';
|
|
6
7
|
export * from './lib/graph-store.js';
|
|
7
8
|
export * from './lib/fingerprint.js';
|
|
8
9
|
export * from './lib/reducer-registry.js';
|
|
@@ -10,6 +11,7 @@ export * from './lib/story.js';
|
|
|
10
11
|
export * from './lib/flow-shape.js';
|
|
11
12
|
export * from './lib/flow-segments.js';
|
|
12
13
|
export * from './lib/flow-categories.js';
|
|
14
|
+
export * from './lib/headless-flow.js';
|
|
13
15
|
export * from './lib/flows-config.js';
|
|
14
16
|
export * from './lib/patterns.js';
|
|
15
17
|
export * from './lib/thresholds.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/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";
|
|
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/answer-budget.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/headless-flow.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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default budget: 24 KiB of UTF-8.
|
|
3
|
+
*
|
|
4
|
+
* Under both hosts measured — Codex kept ~46 KB of a longer answer, Claude Code refused one of
|
|
5
|
+
* 55,759 characters — with room for the envelope the host wraps a result in. In BYTES, not
|
|
6
|
+
* characters: a workspace that logs in Hebrew spends two bytes a character, and a host counts
|
|
7
|
+
* tokens, which follow bytes more closely than they follow UTF-16 units.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_ANSWER_BUDGET_BYTES: number;
|
|
10
|
+
/** Overrides the budget. `0` turns it off (the whole answer, whatever its size). */
|
|
11
|
+
export declare const ANSWER_BUDGET_ENV = "LENSMCP_ANSWER_BUDGET_BYTES";
|
|
12
|
+
/** A budget below this cannot hold the note that says an answer was cut, so it is raised to it. */
|
|
13
|
+
export declare const MIN_ANSWER_BUDGET_BYTES: number;
|
|
14
|
+
/** The key a cut document carries its note under. */
|
|
15
|
+
export declare const ANSWER_CUT_KEY = "cutToFit";
|
|
16
|
+
/** Longest string left whole in an item of a list that is over its budget — a message and about fifteen stack frames. */
|
|
17
|
+
export declare const FITTED_STRING_CHARS = 1500;
|
|
18
|
+
/** A string at least this long is stated once per list; a repeat becomes a reference to the first. */
|
|
19
|
+
export declare const REPEATED_STRING_MIN_CHARS = 200;
|
|
20
|
+
/** The configured budget, in bytes; `Infinity` when it is turned off. */
|
|
21
|
+
export declare function answerBudgetBytes(env?: Record<string, string | undefined>): number;
|
|
22
|
+
/** UTF-8 length of a string, without allocating its encoding. */
|
|
23
|
+
export declare function utf8Bytes(text: string): number;
|
|
24
|
+
/** UTF-8 length of a value as JSON — what it costs on the wire. `0` for what JSON omits. */
|
|
25
|
+
export declare function jsonBytes(value: unknown): number;
|
|
26
|
+
export type ListEnd = 'start' | 'end';
|
|
27
|
+
/**
|
|
28
|
+
* Which end of a list holds its NEWEST items, read off the items themselves.
|
|
29
|
+
*
|
|
30
|
+
* The lens's lists do not agree on an order: an append ring is oldest-first, a "recent" view is
|
|
31
|
+
* often newest-first, and a ranking is neither. Cutting the wrong end of a timeline keeps its
|
|
32
|
+
* oldest entries and drops the ones being asked about, so the end is decided by the times the
|
|
33
|
+
* items carry.
|
|
34
|
+
*
|
|
35
|
+
* A list is a timeline when ONE of its time fields runs in order across the whole of it. Which
|
|
36
|
+
* field differs: a ring fills in arrival order, which is when a span or a request ENDED, so its
|
|
37
|
+
* start times are shuffled (measured: 25 rising / 14 falling) while its end times are not
|
|
38
|
+
* (45 / 0). A ranking has no such field — sorted by slowness, its times agree on a direction
|
|
39
|
+
* about half the time — and its top stays its top.
|
|
40
|
+
*/
|
|
41
|
+
export declare function newestEndOf(items: readonly unknown[]): ListEnd | undefined;
|
|
42
|
+
/** The list newest-first when it is a timeline, as it is otherwise. Never the input array. */
|
|
43
|
+
export declare function newestFirst<T>(items: readonly T[]): T[];
|
|
44
|
+
/** Keep the head of a long string: cut at a line end when one is near, never inside a surrogate pair. */
|
|
45
|
+
export declare function shortenText(text: string, maxChars: number): string;
|
|
46
|
+
export interface ListFitOptions {
|
|
47
|
+
/**
|
|
48
|
+
* The end that holds the newest items, when the caller knows and the items cannot say (an append
|
|
49
|
+
* ring of things that carry no time). Read off the items otherwise ({@link newestEndOf}).
|
|
50
|
+
*/
|
|
51
|
+
newest?: ListEnd;
|
|
52
|
+
/** The end to keep of a list that is not a timeline. Default: its start — the top of a ranking. */
|
|
53
|
+
keep?: ListEnd;
|
|
54
|
+
/** What the list is called in a "same text as" reference. Default `items`. */
|
|
55
|
+
name?: string;
|
|
56
|
+
/** Longest string left whole once the list is over its budget. Default {@link FITTED_STRING_CHARS}. */
|
|
57
|
+
maxStringChars?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface ListFit<T> {
|
|
60
|
+
/** The kept items, in the order the list had them. */
|
|
61
|
+
items: T[];
|
|
62
|
+
/** How many the list held. */
|
|
63
|
+
total: number;
|
|
64
|
+
/** How many are in `items`. */
|
|
65
|
+
returned: number;
|
|
66
|
+
/** Which of them: `all`, the `newest` of a timeline, or the `first` / `last` of a list with no time order. */
|
|
67
|
+
kept: 'all' | 'newest' | 'first' | 'last';
|
|
68
|
+
/** Strings cut to their head. */
|
|
69
|
+
shortenedStrings: number;
|
|
70
|
+
/** Strings replaced by a reference to an identical one stated earlier in the list. */
|
|
71
|
+
repeatedStrings: number;
|
|
72
|
+
/** UTF-8 bytes of `items` as JSON. */
|
|
73
|
+
bytes: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Fit a list into a byte budget, keeping whole items from its fresh end.
|
|
77
|
+
*
|
|
78
|
+
* A list that already fits is returned as it is — same items, nothing copied. Over the budget,
|
|
79
|
+
* each item is copied with its long strings shortened and its repeated ones referenced, and items
|
|
80
|
+
* are admitted from the fresh end until the next one would not fit. The first is always admitted:
|
|
81
|
+
* an answer of one clamped item says more than an empty list.
|
|
82
|
+
*/
|
|
83
|
+
export declare function fitList<T>(items: readonly T[], budgetBytes: number, opts?: ListFitOptions): ListFit<T>;
|
|
84
|
+
/** Split a budget between several claims: a small one is granted whole, and the large ones share what is left evenly. */
|
|
85
|
+
export declare function shareBudget(sizes: readonly number[], budgetBytes: number): number[];
|
|
86
|
+
/** What a cut answer says about itself, under {@link ANSWER_CUT_KEY}. */
|
|
87
|
+
export interface AnswerCut {
|
|
88
|
+
/** The budget it was fitted to. */
|
|
89
|
+
limitBytes: number;
|
|
90
|
+
/** What the whole answer weighs. */
|
|
91
|
+
originalBytes: number;
|
|
92
|
+
/** Each list that lost items: how many it holds, how many are shown, and which. */
|
|
93
|
+
lists: Array<{
|
|
94
|
+
list: string;
|
|
95
|
+
total: number;
|
|
96
|
+
returned: number;
|
|
97
|
+
kept: 'newest' | 'first' | 'last';
|
|
98
|
+
}>;
|
|
99
|
+
shortenedStrings: number;
|
|
100
|
+
repeatedStrings: number;
|
|
101
|
+
/** Cutting lists was not enough: large values elsewhere in the document were shrunk too. */
|
|
102
|
+
clamped?: true;
|
|
103
|
+
note: string;
|
|
104
|
+
}
|
|
105
|
+
export interface AnswerFitOptions {
|
|
106
|
+
/** Where the rest can be read — appended to the note (e.g. the tool that pages this list). */
|
|
107
|
+
more?: string;
|
|
108
|
+
/** Per top-level list: the end that holds its newest items, for a ring whose items carry no time. */
|
|
109
|
+
newest?: Readonly<Record<string, ListEnd>>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Fit a whole answer into the budget.
|
|
113
|
+
*
|
|
114
|
+
* An answer that fits is returned as it is, the SAME value. Over the budget, its top-level lists
|
|
115
|
+
* are cut ({@link fitList}; small lists whole, large ones sharing what the rest of the document
|
|
116
|
+
* leaves), and the result carries {@link ANSWER_CUT_KEY} as its first key. A document that is
|
|
117
|
+
* large somewhere other than a list is shrunk by the event clamp, which keeps its shape.
|
|
118
|
+
*/
|
|
119
|
+
export declare function fitAnswer<T>(value: T, budgetBytes: number, opts?: AnswerFitOptions): {
|
|
120
|
+
value: T;
|
|
121
|
+
cut?: AnswerCut;
|
|
122
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var W=Object.defineProperty;var d=(e,t)=>W(e,"name",{value:t,configurable:!0});var Y=Object.defineProperty,f=d((e,t)=>Y(e,"name",{value:t,configurable:!0}),"a");import{boundedClone as G}from"@lensmcp/protocol-types";export const DEFAULT_ANSWER_BUDGET_BYTES=24*1024,ANSWER_BUDGET_ENV="LENSMCP_ANSWER_BUDGET_BYTES",MIN_ANSWER_BUDGET_BYTES=2*1024,ANSWER_CUT_KEY="cutToFit",FITTED_STRING_CHARS=1500,REPEATED_STRING_MIN_CHARS=200;const j=900,L=32,z=new Set(["timestamp","startedAt","startTime","at","emittedAt","lastSeenAt","endedAt","endTime","t","ts","time"]),V=f(e=>typeof e=="number"&&e>=1e11&&e<=1e14,"isEpochMs"),H=.95;export function answerBudgetBytes(e=process.env){const t=e[ANSWER_BUDGET_ENV];if(t===void 0||t.trim()==="")return DEFAULT_ANSWER_BUDGET_BYTES;const r=Number(t);return!Number.isFinite(r)||r<0?DEFAULT_ANSWER_BUDGET_BYTES:r===0?Number.POSITIVE_INFINITY:Math.max(MIN_ANSWER_BUDGET_BYTES,Math.floor(r))}d(answerBudgetBytes,"answerBudgetBytes"),f(answerBudgetBytes,"answerBudgetBytes");export function utf8Bytes(e){let t=0;for(let r=0;r<e.length;r++){const i=e.charCodeAt(r);i<128?t+=1:i<2048?t+=2:i>=55296&&i<=56319&&r+1<e.length&&(e.charCodeAt(r+1)&64512)===56320?(t+=4,r+=1):t+=3}return t}d(utf8Bytes,"utf8Bytes"),f(utf8Bytes,"utf8Bytes");export function jsonBytes(e){let t;try{t=JSON.stringify(e)}catch{return Number.POSITIVE_INFINITY}return t===void 0?0:utf8Bytes(t)}d(jsonBytes,"jsonBytes"),f(jsonBytes,"jsonBytes");const O=f(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isRecord");function R(e){if(!O(e))return!1;const t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}d(R,"D"),f(R,"isPlain");export function newestEndOf(e){const t=e[0];if(e.length<2||!O(t))return;let r;for(const i of Object.keys(t)){if(typeof t[i]!="number"||!(z.has(i)||V(t[i])))continue;let s=0,h=0,l=!0,g=t[i];for(let E=1;E<e.length;E++){const w=e[E],p=O(w)?w[i]:void 0;if(typeof p!="number"){l=!1;break}p>g?s+=1:p<g&&(h+=1),g=p}const y=s+h;if(!l||y===0)continue;const x=Math.max(s,h)/y;x>=H&&(!r||x>r.agreement)&&(r={end:s>=h?"end":"start",agreement:x})}return r?.end}d(newestEndOf,"newestEndOf"),f(newestEndOf,"newestEndOf");export function newestFirst(e){return newestEndOf(e)==="end"?[...e].reverse():e.slice()}d(newestFirst,"newestFirst"),f(newestFirst,"newestFirst");export function shortenText(e,t){if(e.length<=t)return e;let r=Math.max(0,t);const i=e.lastIndexOf(`
|
|
2
|
+
`,r);return i>=r/2&&(r=i),r>0&&(e.charCodeAt(r-1)&64512)===55296&&(r-=1),`${e.slice(0,r)}\u2026[lensmcp truncated: ${e.length-r} more chars]`}d(shortenText,"shortenText"),f(shortenText,"shortenText");export function fitList(e,t,r={}){const i=e.length,s=e.reduce((o,u)=>o+jsonBytes(u)+1,2);if(s<=t)return{items:e.slice(),total:i,returned:i,kept:"all",shortenedStrings:0,repeatedStrings:0,bytes:s};const h=r.newest??newestEndOf(e),l=h??r.keep??"start",g=r.name??"items",y=r.maxStringChars??FITTED_STRING_CHARS,x=l==="start"?e:[...e].reverse(),E=String(Math.max(0,i-1)).length,w=new Map,p=[],c=[];let S=2,b=0,$=0;for(let o=0;o<x.length;o++){const u=new Map,_=[];let M=0;const v=f((a,N,I,P,U)=>{if(typeof a=="string"){if(a.length>=REPEATED_STRING_MIN_CHARS){const m=w.get(a)??u.get(a);if(m)return _.push({holder:P,key:U,owner:m.owner,path:m.path}),`\u2026[lensmcp: same text as ${g}[${"9".repeat(E)}]${m.path}]`;u.set(a,{owner:o,path:N})}return a.length>y?(M+=1,shortenText(a,y)):a}if(I>=L)return a;if(Array.isArray(a)){const m=[];return a.forEach((T,D)=>m.push(v(T,`${N}[${D}]`,I+1,m,D))),m}if(R(a)){const m={};for(const T of Object.keys(a))m[T]=v(a[T],`${N}.${T}`,I+1,m,T);return m}return a},"slim"),F=[];let k=v(x[o],"",0,F,0),B=jsonBytes(k)+1;if(c.length===0&&S+B>t)k=C(x[o],Math.max(0,t-S-1)),B=jsonBytes(k)+1,_.length=0,u.clear(),M=1;else if(S+B>t)break;c.push(k),S+=B,b+=M,$+=_.length;for(const[a,N]of u)w.set(a,N);for(const a of _)p.push(a.holder===F?{...a,holder:c,key:c.length-1}:a)}const A=c.length,n=f(o=>l==="start"?o:A-1-o,"indexOf");if(l==="end"){for(const o of p)o.holder===c&&(o.key=n(o.key));c.reverse()}for(const o of p)o.holder[o.key]=`\u2026[lensmcp: same text as ${g}[${n(o.owner)}]${o.path}]`;return{items:c,total:i,returned:A,kept:A===i?"all":h?"newest":l==="start"?"first":"last",shortenedStrings:b,repeatedStrings:$,bytes:jsonBytes(c)}}d(fitList,"fitList"),f(fitList,"fitList");function C(e,t){let r=Math.max(256,t),i=e;for(let s=0;s<6;s++){if(i=G(e,{rawByteLimit:r,maxStringBytes:Math.min(2e3,Math.max(128,Math.floor(r/4))),maxArrayItems:64}).value,jsonBytes(i)<=t)return i;r=Math.floor(r*.6)}return i}d(C,"Y"),f(C,"clampTo");export function shareBudget(e,t){const r=e.map((h,l)=>({size:h,index:l})).sort((h,l)=>h.size-l.size),i=new Array(e.length).fill(0);let s=Math.max(0,t);return r.forEach(({size:h,index:l},g)=>{const y=Math.floor(s/(r.length-g));i[l]=Math.min(h,y),s-=i[l]}),i}d(shareBudget,"shareBudget"),f(shareBudget,"shareBudget");export function fitAnswer(e,t,r={}){if(!Number.isFinite(t)||e===null||typeof e!="object")return{value:e};const i=jsonBytes(e);if(i<=t)return{value:e};const s={limitBytes:t,originalBytes:i,lists:[],shortenedStrings:0,repeatedStrings:0,note:""},h=f(()=>{const n=s.lists.map(o=>`\`${o.list}\` shows the ${o.kept} ${o.returned} of ${o.total}`);s.note=`Cut to fit one tool result (${t} bytes; the whole answer is ${Number.isFinite(i)?i:"larger"}) \u2014 it is still one valid JSON document.`+(n.length?` ${n.join("; ")}; any other count in this document describes the whole list.`:"")+(s.shortenedStrings?` ${s.shortenedStrings} long text${s.shortenedStrings===1?" was":"s were"} cut to the head.`:"")+(s.repeatedStrings?` ${s.repeatedStrings} repeated text${s.repeatedStrings===1?" is":"s are"} stated once and referenced.`:"")+(s.clamped?" Large values outside the lists were shrunk too (see `__lensmcpTruncation`).":"")+(r.more?` ${r.more}`:"")+` Whole answer: start the lens with ${ANSWER_BUDGET_ENV}=0.`},"describe");if(Array.isArray(e)){const n=fitList(e,t);return n.kept!=="all"&&s.lists.push({list:"items",total:n.total,returned:n.returned,kept:n.kept}),s.shortenedStrings=n.shortenedStrings,s.repeatedStrings=n.repeatedStrings,h(),{value:n.items,cut:s}}if(!R(e))return{value:e};const l=Object.keys(e),g=l.filter(n=>Array.isArray(e[n])&&e[n].length>0),y=g.map(n=>jsonBytes(e[n])),x=i-y.reduce((n,o)=>n+o,0),E=shareBudget(y,t-x-j),w={};g.forEach((n,o)=>{if(y[o]<=E[o])return;const u=fitList(e[n],E[o],{name:n,...r.newest?.[n]?{newest:r.newest[n]}:{}});w[n]=u.items,u.kept!=="all"&&s.lists.push({list:n,total:u.total,returned:u.returned,kept:u.kept}),s.shortenedStrings+=u.shortenedStrings,s.repeatedStrings+=u.repeatedStrings});const p=f(n=>(h(),{[ANSWER_CUT_KEY]:s,...n}),"assemble"),c={};for(const n of l)c[n]=n in w?w[n]:e[n];let S=p(c);if(jsonBytes(S)<=t)return{value:S,cut:s};s.clamped=!0;let b=Math.max(512,t-j);for(let n=0;n<6;n++){if(S=p(C(c,b)),jsonBytes(S)<=t)return{value:S,cut:s};b=Math.floor(b*.6)}const $={};let A=j;for(const n of l){const o=e[n];if(o!==null&&typeof o=="object")continue;const u=jsonBytes(o)+utf8Bytes(n)+4;if(A+u>t)break;$[n]=o,A+=u}return{value:p($),cut:s}}d(fitAnswer,"fitAnswer"),f(fitAnswer,"fitAnswer");
|
package/lib/flow-categories.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var O=Object.defineProperty;var u=(e,t)=>O(e,"name",{value:t,configurable:!0});var M=Object.defineProperty,s=u((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)}u(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)}u(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))))}u(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),c=l(o.originNodeId)??l(n.context?.originNodeId);return{...i?{type:i}:{},...c?{nodeId:c}:{}}}u(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",request:"api",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("/")}u(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}u($,"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}u(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 c=r.segments.find(g=>g.kind==="request"&&g.route);if(c?.route)return`${c.method??"GET"} ${templatePath(c.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}u(x,"q"),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()}u(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),c=s((d,q,b)=>{const h=x(d,n,t,e);return{category:d,decidedBy:q,...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 c(r[a].category,"rule",a);if(S(t.flowId,n.nodeId))return c("test","test");const g=n.type?I[n.type]:void 0;if(g)return c(g,"origin");if(i.includes("ai"))return c("ai","shape");const v=t.segments.filter(d=>d.kind==="request");return v.length>0?c(v.every(d=>d.requestId?.startsWith("job:"))?"background":"api","shape"):t.segments.length>0&&t.segments.every(d=>d.kind==="ambient")?c("system","shape"):c("other","shape")}u(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"}u(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}})}u(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}))}u(groupByCategory,"groupByCategory"),s(groupByCategory,"groupByCategory");
|
package/lib/flow-shape.d.ts
CHANGED
|
@@ -36,10 +36,16 @@ export declare function trimFlowEvent(event: BaseEvent, opts?: FlowShapeOptions)
|
|
|
36
36
|
commitId?: string | undefined;
|
|
37
37
|
parentRequestId?: string | undefined;
|
|
38
38
|
requestId?: string | undefined;
|
|
39
|
+
stream?: string | undefined;
|
|
40
|
+
logger?: string | undefined;
|
|
41
|
+
level?: string | undefined;
|
|
39
42
|
role?: string | undefined;
|
|
43
|
+
captured?: string[] | undefined;
|
|
44
|
+
error?: string | undefined;
|
|
45
|
+
outcome?: "returned" | "threw" | undefined;
|
|
40
46
|
source: "nx" | "vite" | "typescript" | "eslint" | "rollup" | "client-runtime" | "chrome" | "react" | "valtio" | "nestjs" | "nextjs" | "db" | "redis" | "queue" | "memory" | "visual" | "perf" | "security" | "gateway" | "external" | "ai" | "lens";
|
|
41
47
|
category: "db" | "memory" | "visual" | "security" | "ai" | "build" | "lint" | "typecheck" | "test" | "runtime" | "render" | "trace" | "state" | "network" | "backend" | "performance" | "deps" | "cluster";
|
|
42
|
-
severity: "
|
|
48
|
+
severity: "debug" | "info" | "warning" | "error" | "fatal";
|
|
43
49
|
title: string;
|
|
44
50
|
kind: string | undefined;
|
|
45
51
|
area: FlowArea;
|
|
@@ -51,6 +57,12 @@ export declare function trimFlowEvent(event: BaseEvent, opts?: FlowShapeOptions)
|
|
|
51
57
|
t: number;
|
|
52
58
|
start: number;
|
|
53
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* How long a call must run past the end of the span that started it before the two are marked (`outlivedParentMs`,
|
|
62
|
+
* `leftRunning`). Under this it is a tail — an audit write finishing a moment after the response — and marking every
|
|
63
|
+
* one of those would teach a reader to ignore the mark.
|
|
64
|
+
*/
|
|
65
|
+
export declare const OUTLIVED_PARENT_MIN_MS = 250;
|
|
54
66
|
/**
|
|
55
67
|
* The pure flow transform: raw correlated events → ordered, parented,
|
|
56
68
|
* aggregated display nodes. No I/O, no server — see module header.
|
|
@@ -79,10 +91,16 @@ export declare function buildFlowDetail(rawEvents: BaseEvent[], opts?: FlowShape
|
|
|
79
91
|
commitId?: string | undefined;
|
|
80
92
|
parentRequestId?: string | undefined;
|
|
81
93
|
requestId?: string | undefined;
|
|
94
|
+
stream?: string | undefined;
|
|
95
|
+
logger?: string | undefined;
|
|
96
|
+
level?: string | undefined;
|
|
82
97
|
role?: string | undefined;
|
|
98
|
+
captured?: string[] | undefined;
|
|
99
|
+
error?: string | undefined;
|
|
100
|
+
outcome?: "returned" | "threw" | undefined;
|
|
83
101
|
source: "nx" | "vite" | "typescript" | "eslint" | "rollup" | "client-runtime" | "chrome" | "react" | "valtio" | "nestjs" | "nextjs" | "db" | "redis" | "queue" | "memory" | "visual" | "perf" | "security" | "gateway" | "external" | "ai" | "lens";
|
|
84
102
|
category: "db" | "memory" | "visual" | "security" | "ai" | "build" | "lint" | "typecheck" | "test" | "runtime" | "render" | "trace" | "state" | "network" | "backend" | "performance" | "deps" | "cluster";
|
|
85
|
-
severity: "
|
|
103
|
+
severity: "debug" | "info" | "warning" | "error" | "fatal";
|
|
86
104
|
title: string;
|
|
87
105
|
kind: string | undefined;
|
|
88
106
|
area: FlowArea;
|
package/lib/flow-shape.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var $e=Object.defineProperty;var S=(d,w)=>$e(d,"name",{value:w,configurable:!0});var ke=Object.defineProperty,h=S((d,w)=>ke(d,"name",{value:w,configurable:!0}),"p");import{redactDeep as ye,redactText as ue}from"./redact.js";import{SQL_ACCOUNT_KIND as qe,sqlAccountingOf as we}from"./flow-segments.js";import{boundedClone as Me}from"@lensmcp/protocol-types";export function areaOf(d){const{source:w,category:n,title:$,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($)?"data":"services":w==="valtio"?"state":n==="network"?"network":"browser"}S(areaOf,"areaOf"),h(areaOf,"areaOf");const de=240,Ie=new Set(["systemPrompt","prompt","prompts","messages","completion","completions","input","output","draft","text","content","result","arguments","params"]),ve=new Set(["tenantId","caller","userId","accountId","email","subject"]),je=8;function ce(d){const w=V(d,0);return Me(ye({redacted:!0,...w}),{rawByteLimit:16*1024}).value}S(ce,"It"),h(ce,"aiProjection");function V(d,w){if(typeof d=="string")return le(d);if(d===null||typeof d!="object")return d;if(w>=je)return;if(Array.isArray(d))return d.slice(0,32).map($=>V($,w+1));const n={};for(const[$,y]of Object.entries(d))if(!ve.has($)){if(Ie.has($)){typeof y=="string"?n[`${$}Chars`]=y.length:Array.isArray(y)?n[`${$}Count`]=y.length:y!=null&&(n[`${$}Present`]=!0);continue}n[$]=V(y,w+1)}return n}S(V,"tt"),h(V,"projectAi");function le(d){return d.length<=de?ue(d):`${ue(d.slice(0,de))}\u2026`}S(le,"qt"),h(le,"clipText");export function trimFlowEvent(d,w={}){const n=d.raw??{},$=n.render??{},y=n.span??{},R=n.request??{},f=h(s=>typeof s=="number"?s:void 0,"num"),C=n.loop??{},x=n.effect??{},T=f(n.durationMs)??f(y.durationMs)??f(R.durationMs)??f(C.durationMs)??f($.actualDurationMs)??f(x.durationMs),m=typeof n.kind=="string"?n.kind:void 0,N=m==="fetch-done"||m==="fetch-error",W=f($.commitTime),Y=m==="render"&&W!==void 0&&Math.abs(W-d.timestamp)<6e4,D=N?d.timestamp:Y?W:f(n.startedAt)??f(y.startTime)??f(R.startedAt)??f(C.startedAt)??(T!==void 0?d.timestamp-T:d.timestamp);let v;if(typeof n.query=="string"){const s=Array.isArray(n.params)?` [${n.params.join(", ")}]`:"",q=typeof n.result=="string"?` \u2192 ${n.result}`:"";v=n.query+s+q}else if(typeof n.command=="string"){const s=typeof n.value=="string"?` \u2192 ${n.value}`:n.hit===!1?" \u2192 (miss)":"";v=n.command+s}else if(m==="state-update"){const s=n.update??{};let q="";try{q=JSON.stringify(s.preview)}catch{}v=`${s.storeId??""}.${s.path??""} = ${q}`}else m==="queue-completed"&&typeof n.returned=="string"?v=`returned ${n.returned}`:m==="queue-enqueued"&&(v=`${n.job??"job"}#${n.jobId??"?"} \u2192 ${n.queue??""}${typeof n.data=="string"?" "+n.data:""}`);let F;if(m==="gateway-request")F=`${n.host??""}${String(n.url??"").split("?")[0]}`;else if(m==="fetch-start"||m==="fetch-done"||m==="fetch-error")try{const s=new URL(String(n.href??n.url??""));F=`${s.hostname}${s.pathname}`}catch{}let L;if(m==="gateway-request"&&Array.isArray(n.steps))L=n.steps.map(s=>{const q=[String(s.step)];return s.project&&q.push(String(s.project)),s.prefix&&q.push(String(s.prefix)),s.mode&&q.push(String(s.mode)+(s.ok===!1?" \u2717":s.ok===!0?" \u2713":"")),s.caller&&q.push("by "+String(s.caller)),s.pod&&q.push(String(s.pod)),typeof s.atMs=="number"&&q.push(`@${s.atMs}ms`),q.join(" ")}),v=L.map(s=>s.replace(/ @\d+ms$/,"")).join(" \u2192 ");else if(m==="egress")v=`${n.method??"GET"} ${n.host??""} \u2192 ${n.status??"?"}`;else if(d.source==="ai"){if(m==="ai-call"){const s=n.usage??{},q=s.promptTokens!==void 0||s.completionTokens!==void 0?` \xB7 ${f(s.promptTokens)??"?"}\u2192${f(s.completionTokens)??"?"} tok`:"",B=n.status!==void 0?` \xB7 ${String(n.status)}`:n.error?" \xB7 failed":"",O=f(n.toolCallCount)?` \xB7 ${f(n.toolCallCount)} tool call${f(n.toolCallCount)===1?"":"s"}`:"",U=f(n.durationMs)!==void 0?` \xB7 ${(f(n.durationMs)/1e3).toFixed(1)}s`:"";v=`${n.provider??""}/${n.model??"?"}${q}${B}${O}${U}${n.stream===!0?" \xB7 stream":""}`}else if(m==="ai-round"){const s=n.model??{},q=n.usage??{},B=Array.isArray(n.toolCalls)?n.toolCalls:[],O=`${f(q.promptTokens)??"?"}\u2192${f(q.completionTokens)??"?"} tok`,U=B.length?` \xB7 ${B.length} tool call${B.length===1?"":"s"}`:"",_=f(n.durationMs)!==void 0?` \xB7 ${(f(n.durationMs)/1e3).toFixed(1)}s`:"";v=`${s.provider??""}/${s.model??""} \xB7 ${O}${U}${_}`}else if(m==="ai-turn-start"){const s=Array.isArray(n.tools)?n.tools:[];v=`skill ${n.skill??""} \xB7 ${n.output??""}${s.length?" \xB7 tools: "+s.join(", "):""}`}else if(m==="ai-tool-call"){const s=n.call??{},q=n.result??{},B=f(n.durationMs)!==void 0?` \xB7 ${(f(n.durationMs)/1e3).toFixed(1)}s`:"";v=`${s.name??"tool"} ${q.error!=null?"\u2717 "+String(q.error):"\u2713"}${B}`}else if(m==="ai-turn-end"){const s=n.usage??{},q=f(n.rounds)!==void 0?` \xB7 ${f(n.rounds)} round${f(n.rounds)===1?"":"s"}`:"",B=f(n.durationMs)!==void 0?` \xB7 ${(f(n.durationMs)/1e3).toFixed(1)}s`:"";v=`${n.ok===!1?"FAILED":"ok"} \xB7 ${f(s.promptTokens)??"?"}+${f(s.completionTokens)??"?"} tok${q}${B}`}}let b;m==="db-query"&&typeof n.query=="string"?b=n.query:m==="redis-op"?b=`${n.op??""} ${String(n.key??"").replace(/\d+/g,"#")}`:m==="egress"?b=`${n.method??"GET"} ${n.host??""}`:m==="ai-call"?b=`ai ${n.provider??""} ${n.model??""} ${n.api??""}`:m==="queue-enqueued"?b=`enqueue ${n.queue??""}`:m==="span"&&typeof y.name=="string"?b=y.name:m==="memory-mutation"&&(b=d.title);const A=d.context??{},G=$.commitId??x.commitId??n.commitId,H=Array.isArray($.why)?$.why.map(s=>typeof s?.type=="string"?s.type:void 0).filter(s=>!!s):[];return{id:d.id,t:d.timestamp,start:D,...b?{sig:b}:{},...F?{urlKey:F}:{},...typeof n.queue=="string"?{queue:n.queue}:{},...n.jobId!=null?{jobId:String(n.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 A.requestId=="string"?{requestId:A.requestId}:{},...typeof A.parentRequestId=="string"?{parentRequestId:A.parentRequestId}:{},...typeof G=="string"?{commitId:G}:{},...typeof $.componentName=="string"?{component:$.componentName}:{},...typeof $.phase=="string"?{phase:$.phase}:{},...H.length?{why:H}:{},...m==="effect-run"&&typeof x.hookLogicalId=="string"?{hook:x.hookLogicalId}:{},...m==="effect-run"&&x.cleanupRan===!0?{cleanup:!0}:{},...m==="render-commit-truncated"?{renderTotal:f(n.total),renderPublished:f(n.published)}:{},...typeof A.projectName=="string"?{project:A.projectName}:typeof n.project=="string"?{project:n.project}:{},...T!==void 0?{durationMs:T}:{},...N&&f(n.startedAt)!==void 0?{fetchStartedAt:f(n.startedAt)}:{},...d.source==="ai"?{ai:w.includeRaw?n:ce(n)}:{},...v?{detail:v}:{},...L?{steps:L}:{}}}S(trimFlowEvent,"trimFlowEvent"),h(trimFlowEvent,"trimFlowEvent");const fe=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}S(pe,"Mt"),h(pe,"kindRank");function K(d){return d.kind==="span"&&d.role==="controller"?3.5:d.kind==="queue-process"?3:pe(d.kind)}S(K,"J"),h(K,"causalRank");export function buildFlowDetail(d,w={}){const n=we(d),$=d.slice().filter(e=>{const r=e.raw??{},t=typeof r.kind=="string"?r.kind:"";if(t==="subscription-added"||t==="subscription-removed"||t==="hook-run"||t==="store-created"||t==="singleton-instance"||t===qe)return!1;const o=r.span;return!(typeof o?.name=="string"&&o.name.startsWith("nest.request "))}).map(e=>trimFlowEvent(e,w)),y=new Set;for(let e=$.length-1;e>=0;e--)y.has($[e].id)?$.splice(e,1):y.add($[e].id);const R=new Map;for(const e of $)(e.kind==="gateway-request"||e.kind==="queue-process")&&e.requestId&&!R.has(e.requestId)&&R.set(e.requestId,e.start);const f=h(e=>e.source!=="react"&&e.source!=="valtio"&&e.requestId&&R.has(e.requestId)?R.get(e.requestId):e.start,"anchorOf");$.sort((e,r)=>f(e)-f(r)||e.start-r.start||K(e)-K(r)||(r.durationMs??0)-(e.durationMs??0)||e.t-r.t);const C=h(e=>(e.kind==="span"||e.kind==="server-request"||e.kind==="gateway-request"||e.kind==="queue-process")&&(e.durationMs??0)>0,"spanLike"),x=$;for(const e of x)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 x)if((e.kind==="gateway-request"||e.kind==="queue-process")&&e.requestId&&e.project){const r=T.get(e.requestId)??[];r.push({project:e.project,start:e.start,end:e.start+(e.durationMs??0)}),T.set(e.requestId,r)}const m=h((e,r)=>{const t=T.get(e);if(t?.length)return t.every(o=>o.project===t[0].project)?t[0].project:t.filter(o=>o.start<=r+1&&o.end>=r-1).sort((o,i)=>i.start-o.start)[0]?.project},"projectOf");for(const e of x)if(!e.project&&e.source==="nestjs"&&e.requestId){const r=m(e.requestId,e.start);r&&(e.project=r)}for(const e of x){let r;const t=e.requestId;for(const o of x){if(o===e||!C(o)||o.group!==e.group||o.requestId!==t)continue;const i=o.project,c=e.project;if(o.kind!=="gateway-request"&&e.kind!=="gateway-request"&&i!==void 0&&c!==void 0&&i!==c)continue;const p=o.durationMs??0,g=e.durationMs??0,a=Math.abs(o.start-e.start)<=fe,u=K(o),l=K(e),k=o.start<=e.start;if(!(o.start+p>e.start&&(k||a&&u<l)&&(!a||(u!==l?u<l:k&&p>g))))continue;const j=r?K(r):-1,M=r?Math.abs(o.start-r.start)<=fe:!1;(!r||!M&&o.start>r.start||M&&u>j||M&&u===j&&(o.durationMs??0)<(r.durationMs??0))&&(r=o)}r&&(e.parent=r.id)}const N=new Map;for(const e of x){if(!e.requestId)continue;const r=N.get(e.requestId);r?r.push(e):N.set(e.requestId,[e])}const W=new Map(x.map(e=>[e.id,e])),Y=h((e,r)=>{const t=(N.get(e)??[]).filter(o=>C(o)&&o.start<=r&&o.start+(o.durationMs??0)>=r);if(t.length!==0)return t.sort((o,i)=>i.start-o.start||(o.durationMs??0)-(i.durationMs??0)),t[0]},"callerNodeFor");for(const e of x){if(!e.parentRequestId||e.kind!=="server-request"&&e.kind!=="gateway-request")continue;const r=e.parent?W.get(e.parent):void 0;if(r&&r.requestId===e.requestId)continue;const t=Y(e.parentRequestId,e.start);t&&t!==e&&t.start<=e.start&&(e.parent=t.id)}const D=x,v=new Map;for(const e of D)e.kind==="gateway-request"&&e.requestId&&!v.has(e.requestId)&&v.set(e.requestId,e);for(const e of D)if(e.group==="server"&&!e.parent&&e.requestId&&e.kind!=="gateway-request"){const r=v.get(e.requestId);r&&r!==e&&r.start<=e.start&&(e.parent=r.id)}const F=new Set,L=new Set,b=[];for(const e of D){if(e.kind!=="gateway-request")continue;if(!e.requestId){b.push(e);continue}const r=N.get(e.requestId)??[];if(r.filter(i=>i.kind==="gateway-request").sort((i,c)=>i.start-c.start)[0]!==e)continue;const t=r.find(i=>i.kind==="fetch-start"),o=r.find(i=>i.kind==="fetch-done"||i.kind==="fetch-error");if(!t&&!o){b.push(e);continue}t&&(e.fetchStartId=t.id,F.add(t.id)),o&&(e.fetchDoneId=o.id,L.add(o.id))}const A=new Map;for(const e of D){if(!e.urlKey)continue;const r=A.get(e.urlKey)??{gws:[],starts:[],dones:[]};e.kind==="gateway-request"?b.includes(e)&&r.gws.push(e):e.kind==="fetch-start"?F.has(e.id)||r.starts.push(e):(e.kind==="fetch-done"||e.kind==="fetch-error")&&(L.has(e.id)||r.dones.push(e)),A.set(e.urlKey,r)}for(const{gws:e,starts:r,dones:t}of A.values()){e.sort((i,c)=>i.start-c.start),r.sort((i,c)=>i.start-c.start);const o=new Set;e.forEach((i,c)=>{const p=r[c];p&&(i.fetchStartId=p.id);const g=p?p.t:i.start;let a,u=1/0;for(const l of t){if(o.has(l.id))continue;const k=l.fetchStartedAt??l.start-(l.durationMs??0),j=Math.abs(k-g);j<u&&(u=j,a=l)}a&&(i.fetchDoneId=a.id,o.add(a.id))})}const G=new Map,H=new Map;for(const e of D)!e.queue||!e.jobId||(e.kind==="queue-enqueued"&&G.set(`${e.queue}#${e.jobId}`,e),e.kind==="queue-process"&&H.set(`${e.queue}#${e.jobId}`,e));for(const e of D){if(!e.queue||!e.jobId)continue;const r=`${e.queue}#${e.jobId}`;if(e.kind==="queue-process"&&!e.parent){const t=G.get(r);t&&t!==e&&t.start<=e.start&&(e.parent=t.id)}else if(e.kind==="queue-active"||e.kind==="queue-completed"||e.kind==="queue-failed"){const t=H.get(r);t&&t!==e&&t.start<=e.start&&(e.parent=t.id)}}let s=x;{const e=new Map;for(const t of s)t.kind==="fetch-start"&&e.set(t.id,t);const r=new Map;for(const t of s){if(t.kind!=="fetch-done"&&t.kind!=="fetch-error")continue;const o=[...e.values()].find(p=>p.requestId&&p.requestId===t.requestId)?.id,i=o?e.get(o):void 0;if(!i)continue;const c=t.fetchStartedAt??i.start;i.title=t.title,i.severity=t.severity,i.durationMs=t.durationMs??Math.max(0,t.t-c),i.kind="fetch",t.detail&&!i.detail&&(i.detail=t.detail),r.set(t.id,i.id)}if(r.size>0){s=s.filter(t=>!r.has(t.id));for(const t of s)for(t.fetchDoneId&&r.has(t.fetchDoneId)&&(t.fetchDoneId=r.get(t.fetchDoneId));t.parent&&r.has(t.parent);)t.parent=r.get(t.parent)}}const q=h(e=>!!e.commitId&&(e.kind==="render"||e.kind==="effect-run"||e.kind==="render-commit-truncated"),"commitMember"),B=h(e=>[...new Set(e)],"uniq"),O=h(e=>B(e.map(r=>r.component).filter(r=>!!r)),"named"),U=h(e=>e.length<=2?e.join(" + "):`${e[0]} + ${e.length-1} more`,"nameList"),_=h(e=>{const r=new Map;for(const t of e)t.component&&r.set(t.component,(r.get(t.component)??0)+1);return[...r].map(([t,o])=>o>1?`${t} \xD7${o}`:t).join(", ")},"tally"),me=h(e=>{if(!e)return"effect";const r=/([^/\\]+?):(\d+)(?::\d+)?$/.exec(e);return r?`${r[1]}:${r[2]}`:e},"shortHook"),X=h(e=>`${me(e.hook)}${e.cleanup?" (cleanup)":""}`,"hookLabel"),Z=h(e=>{const r=Math.min(...e.map(o=>o.start)),t=Math.max(...e.map(o=>o.start+(o.durationMs??0)));return{start:r,durationMs:t-r}},"extent"),ee=h(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 r=e[0],t=e.filter(I=>I.kind==="render"),o=e.filter(I=>I.kind==="effect-run"),i=e.find(I=>I.kind==="render-commit-truncated"),c=[...t].sort((I,z)=>(z.durationMs??0)-(I.durationMs??0)),p=c.filter(I=>(I.why??[]).some(z=>z!=="parent-render")),g=B(p.flatMap(I=>(I.why??[]).filter(z=>z!=="parent-render"))),a=O(p),u=t.filter(I=>I.phase==="mount"),l=t.filter(I=>I.phase!=="mount"&&!p.includes(I)),k=c[0]?.component??"view",j=t.length>0&&u.length===t.length,M=g.length?g.join(" + "):void 0,P=j?"mount":"update",E=a.length?`view update \xB7 ${U(a)} \u2190 ${M}`:j?`view mount \xB7 ${k}`:`view update \xB7 ${k}`,ie=i?.renderTotal??t.length,ge=[u.length?`mounted ${_(u)}`:"",l.length?`re-rendered ${_(l)}`:"",!a.length&&!j&&t.length?"trigger not recorded \u2014 every captured render came from its parent":"",i?`${ie} renders, ${i.renderPublished??t.length} recorded (per-commit cap)`:`${t.length} render${t.length===1?"":"s"}`,o.length?`${o.length} effect${o.length===1?"":"s"}: ${o.map(X).join(", ")}`:""].filter(Boolean).join(" \xB7 "),ae=O(t);return ee({...r,...Z(e),kind:"view-update",title:E,detail:ge,commitId:r.commitId,viewPhase:P,...M?{cause:M}:{},causeComponents:a,components:ae,mounted:O(u),renderCount:ie,recordedRenders:t.length,effects:o.length,effectHooks:o.map(X),memberIds:e.map(I=>I.id),sig:`commit:${P}:${M??"-"}:${a.join(",")}:${ae.slice().sort().join(",")}`,severity:e.some(I=>I.severity==="error")?"error":r.severity})}S(te,"dt"),h(te,"viewUpdateNode");function re(e){const r=e[0],t=e.map(X);return ee({...r,...Z(e),kind:"commit-effects",title:e.length===1?`effect \xB7 ${t[0]}`:`effects \xB7 ${t[0]} + ${e.length-1} more`,detail:t.join(", "),commitId:r.commitId,effects:e.length,effectHooks:t,memberIds:e.map(o=>o.id),sig:`effects:${t.slice().sort().join(",")}`})}S(re,"ut"),h(re,"effectsNode");const he=h(e=>B(e.map(r=>r==="state-update"?"state update":r==="fetch-start"?"request":r==="flow-origin"?"interaction":"response")).join(" + "),"describeInputs");function oe(e,r){const t=e[0],o=e[e.length-1],i=e.length,c=t.detail,p=Math.min(...e.map(l=>l.start)),g=Math.max(...e.map(l=>l.start+(l.durationMs??0))),a=Math.round(e.reduce((l,k)=>l+(k.durationMs??0),0)/i*10)/10,u=Math.round((g-p)*10)/10;t.count=i,t.avgMs=a,t.wallMs=u,t.durationMs=g-p,t.items=e.slice(0,12).map(l=>({start:l.start-p,...l.durationMs!=null?{durationMs:l.durationMs}:{}})),t.memberIds=e.flatMap(l=>l.memberIds??[l.id]),t.title=`\xD7${i} ${t.title}`,e.some(l=>l.severity==="error")&&(t.severity="error"),t.kind==="view-update"?t.detail=[`${i} commits, ${r.length?`one per ${he(r)}`:"back to back"}`,`avg ${a}ms \xB7 wall ${u}ms`,c??""].filter(Boolean).join(" \xB7 "):t.kind==="state-update"&&c&&o.detail?t.detail=`${i} updates in a row \xB7 wall ${u}ms \xB7 first ${c} \xB7 last ${o.detail}`:t.detail=[`${i} in a row \xB7 wall ${u}ms`,c??""].filter(Boolean).join(" \xB7 ")}S(oe,"lt"),h(oe,"collapseRepeat");const J=[],Q=new Map,ne=new Set;for(let e=0;e<s.length;e++){if(ne.has(e))continue;const r=s[e];if(!q(r)){J.push(r);continue}const t=[e];for(let c=e+1;c<s.length;c++){const p=s[c];if(p.group==="browser"){if(!q(p)||p.commitId!==r.commitId)break;t.push(c)}}for(const c of t)ne.add(c);const o=t.map(c=>s[c]),i=o.some(c=>c.kind!=="effect-run")?te(o):re(o);for(const c of o)c.id!==i.id&&Q.set(c.id,i.id);J.push(i)}if(Q.size>0||J.some(e=>e.kind==="view-update"||e.kind==="commit-effects")){s=J;for(const e of s)for(;e.parent&&Q.has(e.parent);)e.parent=Q.get(e.parent)}{const e=new Map;for(const r of s)r.kind==="view-update"&&r.commitId&&!e.has(r.commitId)&&e.set(r.commitId,r);for(const r of s){if(r.kind!=="commit-effects"||r.parent||!r.commitId)continue;const t=e.get(r.commitId);t&&t.start<=r.start&&(r.parent=t.id)}}{const e=new Set(["state-update","fetch","fetch-start","fetch-done","fetch-error","flow-origin"]),r=s.map((a,u)=>a.group==="browser"?u:-1).filter(a=>a>=0),t=h(a=>`${a.kind}|${a.sig??a.title}`,"keyOf"),o=r.map(a=>t(s[a])),i=h((a,u,l)=>{for(let k=0;k<l;k++)if(o[a+k]!==o[u+k])return!1;return!0},"sameBlock"),c=h(a=>o[a].split("|")[0],"kindAt"),p=h(a=>{let u=0,l=1;for(let k=1;k<=4&&a+k<=o.length;k++){let j=1;for(;a+(j+1)*k<=o.length&&i(a,a+j*k,k);)j+=1;j>=3&&k*j>u*l&&(u=k,l=j)}return{k:u,r:l}},"bestBlockAt"),g=new Map;for(let a=0;a<o.length;){const{k:u,r:l}=p(a);if(u===0){a+=1;continue}const k=[...Array(u).keys()].find(M=>e.has(c(a+M)));if(k!==void 0&&k>0){const M=p(a+k);if(M.k===u&&M.r>=3&&M.r>=l-1){a+=k;continue}}const j=o.slice(a,a+u).map(M=>M.split("|")[0]).filter(M=>e.has(M));for(let M=0;M<u;M++){const P=[];for(let E=0;E<l;E++)P.push(s[r[a+M+E*u]]);oe(P,j);for(const E of P.slice(1))g.set(E.id,P[0].id)}a+=u*l}if(g.size>0){s=s.filter(a=>!g.has(a.id));for(const a of s)for(;a.parent&&g.has(a.parent);)a.parent=g.get(a.parent)}}{const e=new Map(s.map(r=>[r.id,r]));for(const r of s){const t=new Set([r.id]);let o=r;for(;o?.parent;){if(t.has(o.parent)){o.parent=void 0;break}t.add(o.parent),o=e.get(o.parent)}}}const se=h(e=>{const r=new Map;for(const o of s){if(!o.sig||C(o)!==e||o.kind==="server-request"||o.kind==="gateway-request"||o.group==="browser")continue;const i=`${o.parent??"root"}|${o.kind}|${o.sig}`,c=r.get(i);c?c.push(o):r.set(i,[o])}const t=new Map;for(const o of r.values()){if(o.length<3)continue;o.sort((u,l)=>u.start-l.start);const i=o[0];let c=!1;for(let u=0;u+1<o.length;u++)if(o[u+1].start<o[u].start+(o[u].durationMs??0)){c=!0;break}const p=i.start,g=Math.max(...o.map(u=>u.start+(u.durationMs??0))),a=o.reduce((u,l)=>u+(l.durationMs??0),0);i.count=o.length,i.parallel=c,i.avgMs=Math.round(a/o.length*10)/10,i.wallMs=g-p,i.durationMs=g-p,i.title=`\xD7${o.length} ${i.title.replace(/\s*\((?:\d+ rows?, )?\d+ms\)$/,"")}`,i.detail=`${i.sig} \xD7${o.length} ${c?"\u2016 parallel":"\u21BB sequential loop"} \xB7 avg ${i.avgMs}ms \xB7 wall ${i.wallMs}ms`,i.items=o.slice(0,12).map(u=>({start:u.start-p,...u.durationMs!=null?{durationMs:u.durationMs}:{}})),i.memberIds=o.flatMap(u=>u.memberIds??[u.id]),o.some(u=>u.severity==="error")&&(i.severity="error");for(const u of o.slice(1))t.set(u.id,i.id)}if(t.size!==0){s=s.filter(o=>!t.has(o.id));for(const o of s)for(;o.parent&&t.has(o.parent);)o.parent=t.get(o.parent)}},"aggregatePass");se(!0),se(!1);for(const e of s){if(!C(e))continue;const r=e.durationMs??0;if(r<40)continue;const t=s.filter(g=>g.parent===e.id&&(g.durationMs??0)>0).map(g=>[g.start,g.start+(g.durationMs??0)]).sort((g,a)=>g[0]-a[0]);let o=0,i=-1,c=-1;for(const[g,a]of t)g>c?(c>i&&(o+=c-i),i=g,c=a):a>c&&(c=a);c>i&&(o+=c-i);const p=Math.max(0,r-o);(p>=50||p/r>=.7&&r>=40)&&(e.selfMs=p,e.detail=`\u26A0 cpu-heavy: ${p}ms self-time of ${r}ms${e.detail?" \xB7 "+e.detail:""}`)}return{events:s,...n?{sql:n}:{}}}S(buildFlowDetail,"buildFlowDetail"),h(buildFlowDetail,"buildFlowDetail");
|
|
1
|
+
"use strict";var yt=Object.defineProperty;var R=(d,v)=>yt(d,"name",{value:v,configurable:!0});var kt=Object.defineProperty,y=R((d,v)=>kt(d,"name",{value:v,configurable:!0}),"$");import{redactDeep as Mt,redactText as rt}from"./redact.js";import{SQL_ACCOUNT_KIND as wt,sqlAccountingOf as vt}from"./flow-segments.js";import{boundedClone as qt}from"@lensmcp/protocol-types";export function areaOf(d){const{source:v,category:n,title:M,kind:m}=d;return v==="ai"?"ai":m?.startsWith("memory")||m==="db-query"?"data":v==="gateway"||m==="gateway-request"?"gateway":m==="queue-process"||m==="queue-active"||m==="queue-completed"||m==="queue-failed"?"services":v==="db"||v==="redis"||v==="queue"||m==="redis-op"||m?.startsWith("queue-")?"data":v==="external"||m==="egress"?"network":v==="nestjs"||v==="nest"?/(?:Repository|Repo|Dao|Store)\./.test(M)?"data":"services":v==="valtio"?"state":n==="network"?"network":"browser"}R(areaOf,"areaOf"),y(areaOf,"areaOf");const ct=240,It=new Set(["systemPrompt","prompt","prompts","messages","completion","completions","input","output","draft","text","content","result","arguments","params"]),xt=new Set(["tenantId","caller","userId","accountId","email","subject"]),bt=8;function lt(d){const v=tt(d,0);return qt(Mt({redacted:!0,...v}),{rawByteLimit:16*1024}).value}R(lt,"It"),y(lt,"aiProjection");function tt(d,v){if(typeof d=="string")return ft(d);if(d===null||typeof d!="object")return d;if(v>=bt)return;if(Array.isArray(d))return d.slice(0,32).map(M=>tt(M,v+1));const n={};for(const[M,m]of Object.entries(d))if(!xt.has(M)){if(It.has(M)){typeof m=="string"?n[`${M}Chars`]=m.length:Array.isArray(m)?n[`${M}Count`]=m.length:m!=null&&(n[`${M}Present`]=!0);continue}n[M]=tt(m,v+1)}return n}R(tt,"ot"),y(tt,"projectAi");function ft(d){return d.length<=ct?rt(d):`${rt(d.slice(0,ct))}\u2026`}R(ft,"qt"),y(ft,"clipText");export function trimFlowEvent(d,v={}){const n=d.raw??{},M=n.render??{},m=n.span??{},C=n.request??{},p=y(a=>typeof a=="number"?a:void 0,"num"),O=n.loop??{},b=n.effect??{},T=p(n.durationMs)??p(m.durationMs)??p(C.durationMs)??p(O.durationMs)??p(M.actualDurationMs)??p(b.durationMs),g=typeof n.kind=="string"?n.kind:void 0,F=g==="fetch-done"||g==="fetch-error",X=p(M.commitTime),et=g==="render"&&X!==void 0&&Math.abs(X-d.timestamp)<6e4,P=F?d.timestamp:et?X:p(n.startedAt)??p(m.startTime)??p(C.startedAt)??p(O.startedAt)??(T!==void 0?d.timestamp-T:d.timestamp);let I;if(typeof n.query=="string"){const a=Array.isArray(n.params)?` [${n.params.join(", ")}]`:"",h=typeof n.result=="string"?` \u2192 ${n.result}`:"";I=n.query+a+h}else if(typeof n.command=="string"){const a=typeof n.value=="string"?` \u2192 ${n.value}`:n.hit===!1?" \u2192 (miss)":"";I=n.command+a}else if(g==="state-update"){const a=n.update??{};let h="";try{h=JSON.stringify(a.preview)}catch{}I=`${a.storeId??""}.${a.path??""} = ${h}`}else if(g==="queue-completed"&&typeof n.returned=="string")I=`returned ${n.returned}`;else if(g==="queue-enqueued")I=`${n.job??"job"}#${n.jobId??"?"} \u2192 ${n.queue??""}${typeof n.data=="string"?" "+n.data:""}`;else if(g==="log"){const a=C.error??{},h=typeof a.stack=="string"?a.stack:typeof n.text=="string"?n.text:void 0;h&&h!==d.title&&(I=h)}let N,U,L,E;if(g==="span"){const a=m.outcome;if(N=a==="threw"||a==="returned"?a:m.status==="error"?"threw":m.status==="ok"?"returned":void 0,N==="threw"){const A=m.error??{},S=typeof A.name=="string"?A.name:void 0,V=typeof A.message=="string"?A.message:typeof d.message=="string"?d.message:void 0;L=S,U=rt([S,V].filter(Boolean).join(": "))||void 0}const h=typeof m.args=="string"?m.args:void 0,B=typeof m.result=="string"?m.result:void 0;if(h!==void 0||B!==void 0){E=[...h!==void 0?["args"]:[],...B!==void 0?["result"]:[]];const A=y(S=>S.length>pt?`${S.slice(0,pt)}\u2026`:S,"short");I=[h!==void 0?`args ${A(h)}`:"",B!==void 0?`returned ${A(B)}`:""].filter(Boolean).join(" \u2192 ")}}let K;if(g==="gateway-request")K=`${n.host??""}${String(n.url??"").split("?")[0]}`;else if(g==="fetch-start"||g==="fetch-done"||g==="fetch-error")try{const a=new URL(String(n.href??n.url??""));K=`${a.hostname}${a.pathname}`}catch{}let _;if(g==="gateway-request"&&Array.isArray(n.steps))_=n.steps.map(a=>{const h=[String(a.step)];return a.project&&h.push(String(a.project)),a.prefix&&h.push(String(a.prefix)),a.mode&&h.push(String(a.mode)+(a.ok===!1?" \u2717":a.ok===!0?" \u2713":"")),a.caller&&h.push("by "+String(a.caller)),a.pod&&h.push(String(a.pod)),typeof a.atMs=="number"&&h.push(`@${a.atMs}ms`),h.join(" ")}),I=_.map(a=>a.replace(/ @\d+ms$/,"")).join(" \u2192 ");else if(g==="egress")I=`${n.method??"GET"} ${n.host??""} \u2192 ${n.status??"?"}`;else if(d.source==="ai"){if(g==="ai-call"){const a=n.usage??{},h=a.promptTokens!==void 0||a.completionTokens!==void 0?` \xB7 ${p(a.promptTokens)??"?"}\u2192${p(a.completionTokens)??"?"} tok`:"",B=n.status!==void 0?` \xB7 ${String(n.status)}`:n.error?" \xB7 failed":"",A=p(n.toolCallCount)?` \xB7 ${p(n.toolCallCount)} tool call${p(n.toolCallCount)===1?"":"s"}`:"",S=p(n.durationMs)!==void 0?` \xB7 ${(p(n.durationMs)/1e3).toFixed(1)}s`:"";I=`${n.provider??""}/${n.model??"?"}${h}${B}${A}${S}${n.stream===!0?" \xB7 stream":""}`}else if(g==="ai-round"){const a=n.model??{},h=n.usage??{},B=Array.isArray(n.toolCalls)?n.toolCalls:[],A=`${p(h.promptTokens)??"?"}\u2192${p(h.completionTokens)??"?"} tok`,S=B.length?` \xB7 ${B.length} tool call${B.length===1?"":"s"}`:"",V=p(n.durationMs)!==void 0?` \xB7 ${(p(n.durationMs)/1e3).toFixed(1)}s`:"";I=`${a.provider??""}/${a.model??""} \xB7 ${A}${S}${V}`}else if(g==="ai-turn-start"){const a=Array.isArray(n.tools)?n.tools:[];I=`skill ${n.skill??""} \xB7 ${n.output??""}${a.length?" \xB7 tools: "+a.join(", "):""}`}else if(g==="ai-tool-call"){const a=n.call??{},h=n.result??{},B=p(n.durationMs)!==void 0?` \xB7 ${(p(n.durationMs)/1e3).toFixed(1)}s`:"";I=`${a.name??"tool"} ${h.error!=null?"\u2717 "+String(h.error):"\u2713"}${B}`}else if(g==="ai-turn-end"){const a=n.usage??{},h=p(n.rounds)!==void 0?` \xB7 ${p(n.rounds)} round${p(n.rounds)===1?"":"s"}`:"",B=p(n.durationMs)!==void 0?` \xB7 ${(p(n.durationMs)/1e3).toFixed(1)}s`:"";I=`${n.ok===!1?"FAILED":"ok"} \xB7 ${p(a.promptTokens)??"?"}+${p(a.completionTokens)??"?"} tok${h}${B}`}}let f;g==="db-query"&&typeof n.query=="string"?f=n.query:g==="redis-op"?f=`${n.op??""} ${String(n.key??"").replace(/\d+/g,"#")}`:g==="egress"?f=`${n.method??"GET"} ${n.host??""}`:g==="ai-call"?f=`ai ${n.provider??""} ${n.model??""} ${n.api??""}`:g==="queue-enqueued"?f=`enqueue ${n.queue??""}`:g==="span"&&typeof m.name=="string"?f=m.name:g==="memory-mutation"&&(f=d.title);const D=d.context??{},W=M.commitId??b.commitId??n.commitId,G=Array.isArray(M.why)?M.why.map(a=>typeof a?.type=="string"?a.type:void 0).filter(a=>!!a):[];return{id:d.id,t:d.timestamp,start:P,...f?{sig:f}:{},...K?{urlKey:K}:{},...typeof n.queue=="string"?{queue:n.queue}:{},...n.jobId!=null?{jobId:String(n.jobId)}:{},source:d.source,category:d.category,severity:d.severity,title:N==="threw"&&typeof m.name=="string"?`${m.name} \u2014 threw${L?` ${L}`:""}${T!==void 0?` after ${T}ms`:""}`:d.title,kind:g,area:areaOf({source:d.source,category:d.category,title:d.title,kind:g}),...N?{outcome:N}:{},...U?{error:U}:{},...E?{captured:E}:{},...typeof m.role=="string"?{role:m.role}:{},...g==="log"&&typeof n.level=="string"?{level:n.level}:{},...g==="log"&&typeof n.logger=="string"?{logger:n.logger}:{},...g==="log"&&typeof n.stream=="string"?{stream:n.stream}:{},...typeof D.requestId=="string"?{requestId:D.requestId}:{},...typeof D.parentRequestId=="string"?{parentRequestId:D.parentRequestId}:{},...typeof W=="string"?{commitId:W}:{},...typeof M.componentName=="string"?{component:M.componentName}:{},...typeof M.phase=="string"?{phase:M.phase}:{},...G.length?{why:G}:{},...g==="effect-run"&&typeof b.hookLogicalId=="string"?{hook:b.hookLogicalId}:{},...g==="effect-run"&&b.cleanupRan===!0?{cleanup:!0}:{},...g==="render-commit-truncated"?{renderTotal:p(n.total),renderPublished:p(n.published)}:{},...typeof D.projectName=="string"?{project:D.projectName}:typeof n.project=="string"?{project:n.project}:{},...T!==void 0?{durationMs:T}:{},...F&&p(n.startedAt)!==void 0?{fetchStartedAt:p(n.startedAt)}:{},...d.source==="ai"?{ai:v.includeRaw?n:lt(n)}:{},...I?{detail:I}:{},..._?{steps:_}:{}}}R(trimFlowEvent,"trimFlowEvent"),y(trimFlowEvent,"trimFlowEvent");const pt=300;export const OUTLIVED_PARENT_MIN_MS=250;const jt=new Set(["egress","db-query","redis-op","ai-call","span","gateway-request","server-request"]),mt=1;function gt(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}R(gt,"jt"),y(gt,"kindRank");function J(d){return d.kind==="span"&&d.role==="controller"?3.5:d.kind==="queue-process"?3:gt(d.kind)}R(J,"Z"),y(J,"causalRank");export function buildFlowDetail(d,v={}){const n=vt(d),M=d.slice().filter(t=>{const e=t.raw??{},r=typeof e.kind=="string"?e.kind:"";if(r==="subscription-added"||r==="subscription-removed"||r==="hook-run"||r==="store-created"||r==="singleton-instance"||r===wt)return!1;const o=e.span;return!(typeof o?.name=="string"&&o.name.startsWith("nest.request "))}).map(t=>trimFlowEvent(t,v)),m=new Set;for(let t=M.length-1;t>=0;t--)m.has(M[t].id)?M.splice(t,1):m.add(M[t].id);const C=new Map;for(const t of M)(t.kind==="gateway-request"||t.kind==="queue-process")&&t.requestId&&!C.has(t.requestId)&&C.set(t.requestId,t.start);const p=y(t=>t.source!=="react"&&t.source!=="valtio"&&t.requestId&&C.has(t.requestId)?C.get(t.requestId):t.start,"anchorOf");M.sort((t,e)=>p(t)-p(e)||t.start-e.start||J(t)-J(e)||(e.durationMs??0)-(t.durationMs??0)||t.t-e.t);const O=y(t=>(t.kind==="span"||t.kind==="server-request"||t.kind==="gateway-request"||t.kind==="queue-process")&&(t.durationMs??0)>0,"spanLike"),b=M;for(const t of b)t.group=t.source==="react"||t.source==="valtio"||t.source==="client-runtime"||t.source==="visual"?"browser":"server";const T=new Map;for(const t of b)if((t.kind==="gateway-request"||t.kind==="queue-process")&&t.requestId&&t.project){const e=T.get(t.requestId)??[];e.push({project:t.project,start:t.start,end:t.start+(t.durationMs??0)}),T.set(t.requestId,e)}const g=y((t,e)=>{const r=T.get(t);if(r?.length)return r.every(o=>o.project===r[0].project)?r[0].project:r.filter(o=>o.start<=e+1&&o.end>=e-1).sort((o,s)=>s.start-o.start)[0]?.project},"projectOf");for(const t of b)if(!t.project&&t.source==="nestjs"&&t.requestId){const e=g(t.requestId,t.start);e&&(t.project=e)}for(const t of b){let e;const r=t.requestId;for(const o of b){if(o===t||!O(o)||o.group!==t.group||o.requestId!==r)continue;const s=o.project,c=t.project;if(o.kind!=="gateway-request"&&t.kind!=="gateway-request"&&s!==void 0&&c!==void 0&&s!==c)continue;const $=o.durationMs??0,k=t.durationMs??0,i=Math.abs(o.start-t.start)<=mt,u=J(o),l=J(t),w=o.start<=t.start;if(!(o.start+$>t.start&&(w||i&&u<l)&&(!i||(u!==l?u<l:w&&$>k))))continue;const j=e?J(e):-1,q=e?Math.abs(o.start-e.start)<=mt:!1;(!e||!q&&o.start>e.start||q&&u>j||q&&u===j&&(o.durationMs??0)<(e.durationMs??0))&&(e=o)}e&&(t.parent=e.id)}const F=new Map;for(const t of b){if(!t.requestId)continue;const e=F.get(t.requestId);e?e.push(t):F.set(t.requestId,[t])}const X=new Map(b.map(t=>[t.id,t])),et=y((t,e)=>{const r=(F.get(t)??[]).filter(o=>O(o)&&o.start<=e&&o.start+(o.durationMs??0)>=e);if(r.length!==0)return r.sort((o,s)=>s.start-o.start||(o.durationMs??0)-(s.durationMs??0)),r[0]},"callerNodeFor");for(const t of b){if(!t.parentRequestId||t.kind!=="server-request"&&t.kind!=="gateway-request")continue;const e=t.parent?X.get(t.parent):void 0;if(e&&e.requestId===t.requestId)continue;const r=et(t.parentRequestId,t.start);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}const P=b,I=new Map;for(const t of P)t.kind==="gateway-request"&&t.requestId&&!I.has(t.requestId)&&I.set(t.requestId,t);for(const t of P)if(t.group==="server"&&!t.parent&&t.requestId&&t.kind!=="gateway-request"){const e=I.get(t.requestId);e&&e!==t&&e.start<=t.start&&(t.parent=e.id)}const N=new Set,U=new Set,L=[];for(const t of P){if(t.kind!=="gateway-request")continue;if(!t.requestId){L.push(t);continue}const e=F.get(t.requestId)??[];if(e.filter(s=>s.kind==="gateway-request").sort((s,c)=>s.start-c.start)[0]!==t)continue;const r=e.find(s=>s.kind==="fetch-start"),o=e.find(s=>s.kind==="fetch-done"||s.kind==="fetch-error");if(!r&&!o){L.push(t);continue}r&&(t.fetchStartId=r.id,N.add(r.id)),o&&(t.fetchDoneId=o.id,U.add(o.id))}const E=new Map;for(const t of P){if(!t.urlKey)continue;const e=E.get(t.urlKey)??{gws:[],starts:[],dones:[]};t.kind==="gateway-request"?L.includes(t)&&e.gws.push(t):t.kind==="fetch-start"?N.has(t.id)||e.starts.push(t):(t.kind==="fetch-done"||t.kind==="fetch-error")&&(U.has(t.id)||e.dones.push(t)),E.set(t.urlKey,e)}for(const{gws:t,starts:e,dones:r}of E.values()){t.sort((s,c)=>s.start-c.start),e.sort((s,c)=>s.start-c.start);const o=new Set;t.forEach((s,c)=>{const $=e[c];$&&(s.fetchStartId=$.id);const k=$?$.t:s.start;let i,u=1/0;for(const l of r){if(o.has(l.id))continue;const w=l.fetchStartedAt??l.start-(l.durationMs??0),j=Math.abs(w-k);j<u&&(u=j,i=l)}i&&(s.fetchDoneId=i.id,o.add(i.id))})}const K=new Map,_=new Map;for(const t of P)!t.queue||!t.jobId||(t.kind==="queue-enqueued"&&K.set(`${t.queue}#${t.jobId}`,t),t.kind==="queue-process"&&_.set(`${t.queue}#${t.jobId}`,t));for(const t of P){if(!t.queue||!t.jobId)continue;const e=`${t.queue}#${t.jobId}`;if(t.kind==="queue-process"&&!t.parent){const r=K.get(e);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}else if(t.kind==="queue-active"||t.kind==="queue-completed"||t.kind==="queue-failed"){const r=_.get(e);r&&r!==t&&r.start<=t.start&&(t.parent=r.id)}}{const t=new Map(b.map(e=>[e.id,e]));for(const e of b){if(!e.parent||!jt.has(e.kind??"")||e.durationMs===void 0)continue;const r=t.get(e.parent);if(!r||r.kind!=="span"||!((r.durationMs??0)>0))continue;const o=Math.round(e.start+e.durationMs-(r.start+r.durationMs));if(o<OUTLIVED_PARENT_MIN_MS)continue;e.outlivedParentMs=o;const s=r.leftRunning??{count:0,longestMs:0};r.leftRunning={count:s.count+1,longestMs:Math.max(s.longestMs,o)}}for(const e of b){const r=[];e.leftRunning&&r.push(`ended before ${e.leftRunning.count===1?"a call":`${e.leftRunning.count} calls`} it started had finished \u2014 ${e.leftRunning.count===1?"it":"the longest"} ran on ${e.leftRunning.longestMs}ms (a timeout that gave up, or work not awaited)`),e.outlivedParentMs!==void 0&&r.push(`ran on ${e.outlivedParentMs}ms after the span that started it had ended`),r.length>0&&(e.detail=[e.detail,...r].filter(Boolean).join(" \xB7 "))}}let f=b;{const t=new Map;for(const r of f)r.kind==="fetch-start"&&t.set(r.id,r);const e=new Map;for(const r of f){if(r.kind!=="fetch-done"&&r.kind!=="fetch-error")continue;const o=[...t.values()].find($=>$.requestId&&$.requestId===r.requestId)?.id,s=o?t.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),e.set(r.id,s.id)}if(e.size>0){f=f.filter(r=>!e.has(r.id));for(const r of f)for(r.fetchDoneId&&e.has(r.fetchDoneId)&&(r.fetchDoneId=e.get(r.fetchDoneId));r.parent&&e.has(r.parent);)r.parent=e.get(r.parent)}}const D=y(t=>!!t.commitId&&(t.kind==="render"||t.kind==="effect-run"||t.kind==="render-commit-truncated"),"commitMember"),W=y(t=>[...new Set(t)],"uniq"),G=y(t=>W(t.map(e=>e.component).filter(e=>!!e)),"named"),a=y(t=>t.length<=2?t.join(" + "):`${t[0]} + ${t.length-1} more`,"nameList"),h=y(t=>{const e=new Map;for(const r of t)r.component&&e.set(r.component,(e.get(r.component)??0)+1);return[...e].map(([r,o])=>o>1?`${r} \xD7${o}`:r).join(", ")},"tally"),B=y(t=>{if(!t)return"effect";const e=/([^/\\]+?):(\d+)(?::\d+)?$/.exec(t);return e?`${e[1]}:${e[2]}`:t},"shortHook"),A=y(t=>`${B(t.hook)}${t.cleanup?" (cleanup)":""}`,"hookLabel"),S=y(t=>{const e=Math.min(...t.map(o=>o.start)),r=Math.max(...t.map(o=>o.start+(o.durationMs??0)));return{start:e,durationMs:r-e}},"extent"),V=y(t=>(delete t.component,delete t.why,delete t.phase,delete t.hook,delete t.cleanup,delete t.renderTotal,delete t.renderPublished,t),"asGroup");function ot(t){const e=t[0],r=t.filter(x=>x.kind==="render"),o=t.filter(x=>x.kind==="effect-run"),s=t.find(x=>x.kind==="render-commit-truncated"),c=[...r].sort((x,Y)=>(Y.durationMs??0)-(x.durationMs??0)),$=c.filter(x=>(x.why??[]).some(Y=>Y!=="parent-render")),k=W($.flatMap(x=>(x.why??[]).filter(Y=>Y!=="parent-render"))),i=G($),u=r.filter(x=>x.phase==="mount"),l=r.filter(x=>x.phase!=="mount"&&!$.includes(x)),w=c[0]?.component??"view",j=r.length>0&&u.length===r.length,q=k.length?k.join(" + "):void 0,z=j?"mount":"update",H=i.length?`view update \xB7 ${a(i)} \u2190 ${q}`:j?`view mount \xB7 ${w}`:`view update \xB7 ${w}`,dt=s?.renderTotal??r.length,$t=[u.length?`mounted ${h(u)}`:"",l.length?`re-rendered ${h(l)}`:"",!i.length&&!j&&r.length?"trigger not recorded \u2014 every captured render came from its parent":"",s?`${dt} 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(A).join(", ")}`:""].filter(Boolean).join(" \xB7 "),ut=G(r);return V({...e,...S(t),kind:"view-update",title:H,detail:$t,commitId:e.commitId,viewPhase:z,...q?{cause:q}:{},causeComponents:i,components:ut,mounted:G(u),renderCount:dt,recordedRenders:r.length,effects:o.length,effectHooks:o.map(A),memberIds:t.map(x=>x.id),sig:`commit:${z}:${q??"-"}:${i.join(",")}:${ut.slice().sort().join(",")}`,severity:t.some(x=>x.severity==="error")?"error":e.severity})}R(ot,"ut"),y(ot,"viewUpdateNode");function nt(t){const e=t[0],r=t.map(A);return V({...e,...S(t),kind:"commit-effects",title:t.length===1?`effect \xB7 ${r[0]}`:`effects \xB7 ${r[0]} + ${t.length-1} more`,detail:r.join(", "),commitId:e.commitId,effects:t.length,effectHooks:r,memberIds:t.map(o=>o.id),sig:`effects:${r.slice().sort().join(",")}`})}R(nt,"ft"),y(nt,"effectsNode");const ht=y(t=>W(t.map(e=>e==="state-update"?"state update":e==="fetch-start"?"request":e==="flow-origin"?"interaction":"response")).join(" + "),"describeInputs");function st(t,e){const r=t[0],o=t[t.length-1],s=t.length,c=r.detail,$=Math.min(...t.map(l=>l.start)),k=Math.max(...t.map(l=>l.start+(l.durationMs??0))),i=Math.round(t.reduce((l,w)=>l+(w.durationMs??0),0)/s*10)/10,u=Math.round((k-$)*10)/10;r.count=s,r.avgMs=i,r.wallMs=u,r.durationMs=k-$,r.items=t.slice(0,12).map(l=>({start:l.start-$,...l.durationMs!=null?{durationMs:l.durationMs}:{}})),r.memberIds=t.flatMap(l=>l.memberIds??[l.id]),r.title=`\xD7${s} ${r.title}`,t.some(l=>l.severity==="error")&&(r.severity="error"),r.kind==="view-update"?r.detail=[`${s} commits, ${e.length?`one per ${ht(e)}`:"back to back"}`,`avg ${i}ms \xB7 wall ${u}ms`,c??""].filter(Boolean).join(" \xB7 "):r.kind==="state-update"&&c&&o.detail?r.detail=`${s} updates in a row \xB7 wall ${u}ms \xB7 first ${c} \xB7 last ${o.detail}`:r.detail=[`${s} in a row \xB7 wall ${u}ms`,c??""].filter(Boolean).join(" \xB7 ")}R(st,"pt"),y(st,"collapseRepeat");const Q=[],Z=new Map,at=new Set;for(let t=0;t<f.length;t++){if(at.has(t))continue;const e=f[t];if(!D(e)){Q.push(e);continue}const r=[t];for(let c=t+1;c<f.length;c++){const $=f[c];if($.group==="browser"){if(!D($)||$.commitId!==e.commitId)break;r.push(c)}}for(const c of r)at.add(c);const o=r.map(c=>f[c]),s=o.some(c=>c.kind!=="effect-run")?ot(o):nt(o);for(const c of o)c.id!==s.id&&Z.set(c.id,s.id);Q.push(s)}if(Z.size>0||Q.some(t=>t.kind==="view-update"||t.kind==="commit-effects")){f=Q;for(const t of f)for(;t.parent&&Z.has(t.parent);)t.parent=Z.get(t.parent)}{const t=new Map;for(const e of f)e.kind==="view-update"&&e.commitId&&!t.has(e.commitId)&&t.set(e.commitId,e);for(const e of f){if(e.kind!=="commit-effects"||e.parent||!e.commitId)continue;const r=t.get(e.commitId);r&&r.start<=e.start&&(e.parent=r.id)}}{const t=new Set(["state-update","fetch","fetch-start","fetch-done","fetch-error","flow-origin"]),e=f.map((i,u)=>i.group==="browser"?u:-1).filter(i=>i>=0),r=y(i=>`${i.kind}|${i.sig??i.title}`,"keyOf"),o=e.map(i=>r(f[i])),s=y((i,u,l)=>{for(let w=0;w<l;w++)if(o[i+w]!==o[u+w])return!1;return!0},"sameBlock"),c=y(i=>o[i].split("|")[0],"kindAt"),$=y(i=>{let u=0,l=1;for(let w=1;w<=4&&i+w<=o.length;w++){let j=1;for(;i+(j+1)*w<=o.length&&s(i,i+j*w,w);)j+=1;j>=3&&w*j>u*l&&(u=w,l=j)}return{k:u,r:l}},"bestBlockAt"),k=new Map;for(let i=0;i<o.length;){const{k:u,r:l}=$(i);if(u===0){i+=1;continue}const w=[...Array(u).keys()].find(q=>t.has(c(i+q)));if(w!==void 0&&w>0){const q=$(i+w);if(q.k===u&&q.r>=3&&q.r>=l-1){i+=w;continue}}const j=o.slice(i,i+u).map(q=>q.split("|")[0]).filter(q=>t.has(q));for(let q=0;q<u;q++){const z=[];for(let H=0;H<l;H++)z.push(f[e[i+q+H*u]]);st(z,j);for(const H of z.slice(1))k.set(H.id,z[0].id)}i+=u*l}if(k.size>0){f=f.filter(i=>!k.has(i.id));for(const i of f)for(;i.parent&&k.has(i.parent);)i.parent=k.get(i.parent)}}{const t=new Map(f.map(e=>[e.id,e]));for(const e of f){const r=new Set([e.id]);let o=e;for(;o?.parent;){if(r.has(o.parent)){o.parent=void 0;break}r.add(o.parent),o=t.get(o.parent)}}}const it=y(t=>{const e=new Map;for(const o of f){if(!o.sig||O(o)!==t||o.kind==="server-request"||o.kind==="gateway-request"||o.group==="browser")continue;const s=`${o.parent??"root"}|${o.kind}|${o.sig}`,c=e.get(s);c?c.push(o):e.set(s,[o])}const r=new Map;for(const o of e.values()){if(o.length<3)continue;o.sort((u,l)=>u.start-l.start);const s=o[0];let c=!1;for(let u=0;u+1<o.length;u++)if(o[u+1].start<o[u].start+(o[u].durationMs??0)){c=!0;break}const $=s.start,k=Math.max(...o.map(u=>u.start+(u.durationMs??0))),i=o.reduce((u,l)=>u+(l.durationMs??0),0);s.count=o.length,s.parallel=c,s.avgMs=Math.round(i/o.length*10)/10,s.wallMs=k-$,s.durationMs=k-$,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(u=>({start:u.start-$,...u.durationMs!=null?{durationMs:u.durationMs}:{}})),s.memberIds=o.flatMap(u=>u.memberIds??[u.id]),o.some(u=>u.severity==="error")&&(s.severity="error");for(const u of o.slice(1))r.set(u.id,s.id)}if(r.size!==0){f=f.filter(o=>!r.has(o.id));for(const o of f)for(;o.parent&&r.has(o.parent);)o.parent=r.get(o.parent)}},"aggregatePass");it(!0),it(!1);for(const t of f){if(!O(t))continue;const e=t.durationMs??0;if(e<40)continue;const r=f.filter(k=>k.parent===t.id&&(k.durationMs??0)>0).map(k=>[k.start,k.start+(k.durationMs??0)]).sort((k,i)=>k[0]-i[0]);let o=0,s=-1,c=-1;for(const[k,i]of r)k>c?(c>s&&(o+=c-s),s=k,c=i):i>c&&(c=i);c>s&&(o+=c-s);const $=Math.max(0,e-o);($>=50||$/e>=.7&&e>=40)&&(t.selfMs=$,t.detail=`\u26A0 cpu-heavy: ${$}ms self-time of ${e}ms${t.detail?" \xB7 "+t.detail:""}`)}return{events:f,...n?{sql:n}:{}}}R(buildFlowDetail,"buildFlowDetail"),y(buildFlowDetail,"buildFlowDetail");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flow of work nobody's browser started — a curl, an e2e runner, a webhook, a queue job.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists (issues/closed/foodguard-59) ────────────────────────────
|
|
5
|
+
* A flow id was minted in ONE place: the page. Every other producer only ever copied the id it was
|
|
6
|
+
* handed — the gateway from `x-lensmcp-flow-id`, a pod from the same header, a queue worker from
|
|
7
|
+
* the stamp its job carried. So a request made without a browser was traced in full and listed
|
|
8
|
+
* nowhere: on one workspace a single API call produced 6,898 events (4,823 spans, 743 queries,
|
|
9
|
+
* 177 service requests) and not one of them carried a flow id, the flow index refuses an event
|
|
10
|
+
* without one, and `flow.list_recent` showed 44 flows — 20 page loads, 23 clicks, one navigation.
|
|
11
|
+
* The classifier already had the categories for the rest (`api`, `background`, `test`). Nothing
|
|
12
|
+
* ever produced a flow for them to classify.
|
|
13
|
+
*
|
|
14
|
+
* ── The rule ────────────────────────────────────────────────────────────────
|
|
15
|
+
* Work that arrives with no flow starts one, and its id is DERIVED from an id its caller already
|
|
16
|
+
* holds — never random:
|
|
17
|
+
*
|
|
18
|
+
* `api:<request id>` a request. The gateway returns the request id on every response
|
|
19
|
+
* (`x-request-id`, and `traceId` in an error body), so whoever made the
|
|
20
|
+
* call can open its flow without listing anything.
|
|
21
|
+
* `job:<queue>#<job id>` a queue job nobody stamped. The id a queue dashboard shows.
|
|
22
|
+
*
|
|
23
|
+
* Derived, so the gateway and the pod agree without talking (each mints from the same request id),
|
|
24
|
+
* and so a retry of one job is one flow. Everything downstream — the east-west calls the request
|
|
25
|
+
* makes, the jobs it enqueues — inherits the id exactly as it would a page's.
|
|
26
|
+
*
|
|
27
|
+
* Dependency-free on purpose: the gateway and the pod taps import this file alone
|
|
28
|
+
* (`@lensmcp/core/headless-flow`), not the core graph.
|
|
29
|
+
*/
|
|
30
|
+
/** Turns the rule off: work with no flow stays flow-less, as before. */
|
|
31
|
+
export declare const HEADLESS_FLOWS_ENV = "LENSMCP_HEADLESS_FLOWS";
|
|
32
|
+
/** On unless `LENSMCP_HEADLESS_FLOWS` is `0` / `false` / `off` / `no`. */
|
|
33
|
+
export declare function headlessFlowsEnabled(env?: Record<string, string | undefined>): boolean;
|
|
34
|
+
/** Prefix of a flow started by a request that carried none. */
|
|
35
|
+
export declare const HEADLESS_REQUEST_FLOW_PREFIX = "api:";
|
|
36
|
+
/** Prefix of a flow started by a queue job that carried none. */
|
|
37
|
+
export declare const HEADLESS_JOB_FLOW_PREFIX = "job:";
|
|
38
|
+
/** The flow of a request that arrived with no flow of its own. */
|
|
39
|
+
export declare function headlessFlowId(requestId: string): string;
|
|
40
|
+
/** The flow of a queue job nobody stamped — the same string as the job's leg id (`job:<queue>#<id>`). */
|
|
41
|
+
export declare function headlessJobFlowId(queue: string, jobId: string | number | undefined): string;
|
|
42
|
+
/** Was this flow started by the lens for work that carried none — rather than by a page, or named by a script? */
|
|
43
|
+
export declare function isHeadlessFlowId(flowId: string): boolean;
|
|
44
|
+
/** A liveness / readiness / metrics path: traced like any request, and not the start of a flow. */
|
|
45
|
+
export declare function isProbePath(path: string | undefined): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var r=Object.defineProperty;var s=(e,t)=>r(e,"name",{value:t,configurable:!0});var a=Object.defineProperty,o=s((e,t)=>a(e,"name",{value:t,configurable:!0}),"t");export const HEADLESS_FLOWS_ENV="LENSMCP_HEADLESS_FLOWS";export function headlessFlowsEnabled(e=process.env){return!["0","false","off","no"].includes((e[HEADLESS_FLOWS_ENV]??"").trim().toLowerCase())}s(headlessFlowsEnabled,"headlessFlowsEnabled"),o(headlessFlowsEnabled,"headlessFlowsEnabled");export const HEADLESS_REQUEST_FLOW_PREFIX="api:",HEADLESS_JOB_FLOW_PREFIX="job:";export function headlessFlowId(e){return`${HEADLESS_REQUEST_FLOW_PREFIX}${e}`}s(headlessFlowId,"headlessFlowId"),o(headlessFlowId,"headlessFlowId");export function headlessJobFlowId(e,t){return`${HEADLESS_JOB_FLOW_PREFIX}${e}#${t??"?"}`}s(headlessJobFlowId,"headlessJobFlowId"),o(headlessJobFlowId,"headlessJobFlowId");export function isHeadlessFlowId(e){return e.startsWith(HEADLESS_REQUEST_FLOW_PREFIX)||e.startsWith(HEADLESS_JOB_FLOW_PREFIX)}s(isHeadlessFlowId,"isHeadlessFlowId"),o(isHeadlessFlowId,"isHeadlessFlowId");const n=/^\/(?:[^/?#]+\/)*(?:health|healthz|health-check|healthcheck|ready|readyz|readiness|live|livez|liveness|ping|metrics|favicon\.ico)\/?(?:[?#].*)?$/i;export function isProbePath(e){return e?n.test(e)||e.startsWith("/__lensmcp/"):!1}s(isProbePath,"isProbePath"),o(isProbePath,"isProbePath");
|
package/lib/patterns.d.ts
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* - n+1-query — the same query signature repeated ≥ N times in a window
|
|
10
10
|
* - slow-path — the longest cumulative duration on the caused/async axis
|
|
11
11
|
* - render-storm — one componentInstanceId rendered > N times in < T ms
|
|
12
|
+
* - abandoned-await — a method ended well before a call it started had finished
|
|
12
13
|
*/
|
|
13
14
|
import type { BaseEvent } from '@lensmcp/protocol-types';
|
|
14
|
-
export type PatternKind = 'db-in-loop' | 'n+1-query' | 'slow-path' | 'render-storm' | 'leak';
|
|
15
|
+
export type PatternKind = 'db-in-loop' | 'n+1-query' | 'slow-path' | 'render-storm' | 'abandoned-await' | 'leak';
|
|
15
16
|
export interface PatternMatch {
|
|
16
17
|
kind: PatternKind;
|
|
17
18
|
severity: 'info' | 'warning' | 'error';
|
|
@@ -32,6 +33,22 @@ export interface DbInLoopOptions {
|
|
|
32
33
|
* attribute matching `LoopAttrs`.
|
|
33
34
|
*/
|
|
34
35
|
export declare function detectDbInLoop(events: readonly BaseEvent[], options?: DbInLoopOptions): PatternMatch[];
|
|
36
|
+
export interface AbandonedAwaitOptions {
|
|
37
|
+
/** How long a call must run past the end of the method that started it. Default 250 ms. */
|
|
38
|
+
thresholdMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The method that was running when `call` began — the innermost span of the same request alive at that moment —
|
|
42
|
+
* if it ENDED at least `thresholdMs` before the call did (issues/closed/foodguard-57).
|
|
43
|
+
*
|
|
44
|
+
* `AssistantSpendCap.check` ends after 3001 ms; the request it made answers 200 after 5686 ms. That is a timeout
|
|
45
|
+
* that gave up on its call — the service then took its fallback path, which changes what a user gets — and nothing
|
|
46
|
+
* in the trace said so. It is also what work started and not awaited looks like; the two are told apart by reading
|
|
47
|
+
* the method, which this names.
|
|
48
|
+
*/
|
|
49
|
+
export declare function abandonedAwaitOf(call: BaseEvent, events: readonly BaseEvent[], options?: AbandonedAwaitOptions): PatternMatch | undefined;
|
|
50
|
+
/** Every abandoned await in a window of events (on demand; a reducer asks {@link abandonedAwaitOf} per arriving call). */
|
|
51
|
+
export declare function detectAbandonedAwait(events: readonly BaseEvent[], options?: AbandonedAwaitOptions): PatternMatch[];
|
|
35
52
|
export interface NPlusOneOptions {
|
|
36
53
|
/** Minimum repeats of an identical signature before flagging. Default 3. */
|
|
37
54
|
threshold?: number;
|
package/lib/patterns.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var w=Object.defineProperty;var u=(i,n)=>w(i,"name",{value:n,configurable:!0});var y=Object.defineProperty,p=u((i,n)=>y(i,"name",{value:n,configurable:!0}),"l");export function detectDbInLoop(i,n={}){const r=n.threshold??5,d=[];for(const t of i){const e=t.raw??null;if(e?.kind!=="loop"||!e.loop)continue;const s=e.loop.dbCallsInsideLoop??0;s>r&&d.push({kind:"db-in-loop",severity:"warning",title:`${s} DB calls inside a loop (${e.loop.iterations??"?"} iterations)`,evidence:[t.id],details:{dbCallsInsideLoop:s,iterations:e.loop.iterations,durationMs:e.loop.durationMs,threshold:r},detectedAt:t.timestamp})}return d}u(detectDbInLoop,"detectDbInLoop"),p(detectDbInLoop,"detectDbInLoop");const j=new Set(["egress","db-query","redis-op","ai-call"]);function b(i){const n=i.raw??null;if(!n||typeof n.kind!="string"||!j.has(n.kind))return;const r=typeof n.durationMs=="number"?n.durationMs:void 0;if(r===void 0)return;const d=typeof n.startedAt=="number"?n.startedAt:i.timestamp-r,t=i.context??{};return{id:i.id,start:d,end:d+r,label:i.title,...typeof t.requestId=="string"?{requestId:t.requestId}:{},...typeof n.project=="string"?{project:n.project}:typeof t.projectName=="string"?{project:t.projectName}:{}}}u(b,"M"),p(b,"timedCall");function g(i){const n=i.raw??null,r=n?.kind==="span"?n.span:void 0;if(!r||typeof r.name!="string"||r.name.startsWith("nest.request "))return;const d=typeof r.durationMs=="number"?r.durationMs:void 0;if(d===void 0||d<=0)return;const t=typeof r.startTime=="number"?r.startTime:i.timestamp-d,e=i.context??{};return{id:i.id,start:t,end:t+d,label:r.name,...typeof e.requestId=="string"?{requestId:e.requestId}:{},...typeof n?.project=="string"?{project:n.project}:typeof e.projectName=="string"?{project:e.projectName}:{}}}u(g,"b"),p(g,"timedSpan");export function abandonedAwaitOf(i,n,r={}){const d=r.thresholdMs??250,t=b(i);if(!t||!t.requestId||t.end-t.start<d)return;let e;for(const o of n){const a=g(o);!a||a.requestId!==t.requestId||a.project!==void 0&&t.project!==void 0&&a.project!==t.project||a.start>t.start||a.end<=t.start||(!e||a.start>e.start||a.start===e.start&&a.end<e.end)&&(e=a)}if(!e)return;const s=Math.round(t.end-e.end);if(!(s<d))return{kind:"abandoned-await",severity:"warning",title:`${e.label} ended ${s}ms before a call it started had finished (${t.label})`,evidence:[e.id,t.id],details:{span:e.label,call:t.label,ranOnMs:s,spanMs:Math.round(e.end-e.start),callMs:Math.round(t.end-t.start),thresholdMs:d,...t.project?{project:t.project}:{}},detectedAt:i.timestamp}}u(abandonedAwaitOf,"abandonedAwaitOf"),p(abandonedAwaitOf,"abandonedAwaitOf");export function detectAbandonedAwait(i,n={}){const r=[];for(const d of i){const t=abandonedAwaitOf(d,i,n);t&&r.push(t)}return r}u(detectAbandonedAwait,"detectAbandonedAwait"),p(detectAbandonedAwait,"detectAbandonedAwait");export function detectNPlusOne(i,n={}){const r=n.threshold??3,d=new Map;for(const e of i){const s=e.raw??null;if(s?.kind!=="db-query"||!s.query?.signature)continue;const o=s.query.signature;let a=d.get(o);a||(a=[],d.set(o,a)),a.push(e)}const t=[];for(const[e,s]of d)s.length>=r&&t.push({kind:"n+1-query",severity:"warning",title:`Query repeated ${s.length}\xD7 \u2014 likely N+1`,evidence:s.map(o=>o.id),details:{signature:e,count:s.length,threshold:r},detectedAt:s[s.length-1].timestamp});return t}u(detectNPlusOne,"detectNPlusOne"),p(detectNPlusOne,"detectNPlusOne");export function detectRenderStorm(i,n={}){const r=n.threshold??5,d=n.windowMs??250,t=new Map;for(const s of i){if(s.source!=="react")continue;const o=s.raw??null;if(o?.kind!=="render"||!o.render?.componentInstanceId)continue;const a=o.render.componentInstanceId;let c=t.get(a);c||(c=[],t.set(a,c)),c.push(s)}const e=[];for(const[s,o]of t){o.sort((c,f)=>c.timestamp-f.timestamp);let a=0;for(let c=0;c<o.length;c++){for(;o[c].timestamp-o[a].timestamp>d;)a++;const f=c-a+1;if(f>r){const l=o.slice(a,c+1),m=l[0].raw.render?.componentName??s;e.push({kind:"render-storm",severity:"warning",title:`${m} rendered ${f}\xD7 in ${d}ms`,evidence:l.map(h=>h.id),details:{componentInstanceId:s,count:f,windowMs:d,threshold:r},detectedAt:l[l.length-1].timestamp});break}}}return e}u(detectRenderStorm,"detectRenderStorm"),p(detectRenderStorm,"detectRenderStorm");export function findSlowPath(i,n,r){const d=new Map,t=new Set;function e(o){const a=d.get(o);if(a)return a;if(t.has(o))return{path:[o],totalDurationMs:r(o)};t.add(o);const c=r(o),f=n.get(o)??[];let l;for(const h of f){const M=e(h);(!l||M.totalDurationMs>l.totalDurationMs)&&(l=M)}t.delete(o);const m=l?{path:[o,...l.path],totalDurationMs:c+l.totalDurationMs}:{path:[o],totalDurationMs:c};return d.set(o,m),m}u(e,"e"),p(e,"best");let s={path:[],totalDurationMs:0};for(const o of i){const a=e(o);a.totalDurationMs>s.totalDurationMs&&(s=a)}return s}u(findSlowPath,"findSlowPath"),p(findSlowPath,"findSlowPath");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -31,10 +31,15 @@
|
|
|
31
31
|
"types": "./lib/ai-content.d.ts",
|
|
32
32
|
"import": "./lib/ai-content.js",
|
|
33
33
|
"default": "./lib/ai-content.js"
|
|
34
|
+
},
|
|
35
|
+
"./headless-flow": {
|
|
36
|
+
"types": "./lib/headless-flow.d.ts",
|
|
37
|
+
"import": "./lib/headless-flow.js",
|
|
38
|
+
"default": "./lib/headless-flow.js"
|
|
34
39
|
}
|
|
35
40
|
},
|
|
36
41
|
"dependencies": {
|
|
37
|
-
"@lensmcp/protocol-types": "1.24.
|
|
42
|
+
"@lensmcp/protocol-types": "1.24.4",
|
|
38
43
|
"tslib": "^2.3.0"
|
|
39
44
|
},
|
|
40
45
|
"license": "Apache-2.0",
|