@lensmcp/react-instrumentation 1.25.5 → 1.25.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +3 -0
- package/index.js +1 -1
- package/lib/fiber-renders.js +1 -1
- package/lib/flow-fetch.js +1 -1
- package/lib/page-perf.d.ts +38 -0
- package/lib/page-perf.js +1 -0
- package/lib/page-visit-runtime.d.ts +15 -0
- package/lib/page-visit-runtime.js +1 -0
- package/lib/page-visits.d.ts +163 -0
- package/lib/page-visits.js +1 -0
- package/lib/publish.d.ts +3 -1
- package/lib/publish.js +1 -1
- package/lib/route-flows.d.ts +13 -2
- package/lib/route-flows.js +1 -1
- package/lib/router-pattern.d.ts +24 -0
- package/lib/router-pattern.js +1 -0
- package/package.json +2 -2
package/index.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export { installRouteFlows, routeNodeId, type RouteFlowEnvironment } from './lib
|
|
|
8
8
|
export { useComponentIdentity } from './lib/component-identity.js';
|
|
9
9
|
export type { ComponentIdentity } from './lib/component-identity.js';
|
|
10
10
|
export { installFiberRenderPublisher, resetFiberRenderPublisherForTests } from './lib/fiber-renders.js';
|
|
11
|
+
export { notePageState, startHardPageVisit, pageVisitOpen } from './lib/page-visit-runtime.js';
|
|
12
|
+
export { PageVisitTracker, QUIET_MS, READY_CAP_MS, NO_COMMIT_MS, END_AFTER_READY_MS, type PageCommit, type VisitClock } from './lib/page-visits.js';
|
|
13
|
+
export { patternOf, routeMatchesOf } from './lib/router-pattern.js';
|
|
11
14
|
export { publish, publishLazy, setPublisher, drainQueue, queuedByteCount, runInFlow, currentFlow, activeFlow, extendFlowWindow, beginBackgroundFlow, flowForNewActivity, configureAmbientFlows, DEFAULT_AMBIENT_FLOW_MAX_MS, FLOW_ORIGIN_FINGERPRINT_PREFIX, withFlow, type AmbientFlowOptions, type BackgroundFlowOrigin, type PublishEnvelope, type PublishCategory, type FlowSpec, } from './lib/publish.js';
|
|
12
15
|
export { componentLogicalId, hookLogicalId, slotLogicalId, makeInstanceId, depsHash, resetIdentityState, } from './lib/identity.js';
|
|
13
16
|
export type { RenderRecord, EffectRecord, HookRecord, SlotChangeRecord, ComponentMountRecord, ReactPublishPayload, } from './lib/types.js';
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";export{LensmcpRoot}from"./lib/lensmcp-root.js";export{LensmcpContext,useLensmcpContext}from"./lib/context.js";export{tracedUseEffect,tracedUseMemo,tracedUseCallback}from"./lib/traced-hooks.js";export{TraceSlot}from"./lib/trace-slot.js";export{withTraceComponent}from"./lib/hoc.js";export{installFlowFetch}from"./lib/flow-fetch.js";export{installRouteFlows,routeNodeId}from"./lib/route-flows.js";export{useComponentIdentity}from"./lib/component-identity.js";export{installFiberRenderPublisher,resetFiberRenderPublisherForTests}from"./lib/fiber-renders.js";export{publish,publishLazy,setPublisher,drainQueue,queuedByteCount,runInFlow,currentFlow,activeFlow,extendFlowWindow,beginBackgroundFlow,flowForNewActivity,configureAmbientFlows,DEFAULT_AMBIENT_FLOW_MAX_MS,FLOW_ORIGIN_FINGERPRINT_PREFIX,withFlow}from"./lib/publish.js";export{componentLogicalId,hookLogicalId,slotLogicalId,makeInstanceId,depsHash,resetIdentityState}from"./lib/identity.js";export{default as lensmcpBabelPlugin}from"./lib/babel-plugin.js";
|
|
1
|
+
"use strict";export{LensmcpRoot}from"./lib/lensmcp-root.js";export{LensmcpContext,useLensmcpContext}from"./lib/context.js";export{tracedUseEffect,tracedUseMemo,tracedUseCallback}from"./lib/traced-hooks.js";export{TraceSlot}from"./lib/trace-slot.js";export{withTraceComponent}from"./lib/hoc.js";export{installFlowFetch}from"./lib/flow-fetch.js";export{installRouteFlows,routeNodeId}from"./lib/route-flows.js";export{useComponentIdentity}from"./lib/component-identity.js";export{installFiberRenderPublisher,resetFiberRenderPublisherForTests}from"./lib/fiber-renders.js";export{notePageState,startHardPageVisit,pageVisitOpen}from"./lib/page-visit-runtime.js";export{PageVisitTracker,QUIET_MS,READY_CAP_MS,NO_COMMIT_MS,END_AFTER_READY_MS}from"./lib/page-visits.js";export{patternOf,routeMatchesOf}from"./lib/router-pattern.js";export{publish,publishLazy,setPublisher,drainQueue,queuedByteCount,runInFlow,currentFlow,activeFlow,extendFlowWindow,beginBackgroundFlow,flowForNewActivity,configureAmbientFlows,DEFAULT_AMBIENT_FLOW_MAX_MS,FLOW_ORIGIN_FINGERPRINT_PREFIX,withFlow}from"./lib/publish.js";export{componentLogicalId,hookLogicalId,slotLogicalId,makeInstanceId,depsHash,resetIdentityState}from"./lib/identity.js";export{default as lensmcpBabelPlugin}from"./lib/babel-plugin.js";
|
package/lib/fiber-renders.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var j=Object.defineProperty;var u=(e,t)=>j(e,"name",{value:t,configurable:!0});var A=Object.defineProperty,m=u((e,t)=>A(e,"name",{value:t,configurable:!0}),"d");import{makeInstanceId as $}from"./identity.js";import{publish as N}from"./publish.js";import{notePageCommit as W}from"./page-visit-runtime.js";import{patternOf as G,routeMatchesOf as K}from"./router-pattern.js";const B=1,U=0,Z=1,q=11,H=13,E=14,X=15,P=15e3,D=25,_=15,Y=8,R=(typeof process<"u"?process.env?.LENSMCP_RENDER_INCLUDE_INTERNALS:void 0)==="1",J=new Set(["Insertion","EmotionGlobal","EmotionCssPropInternal","GlobalStyles","ThemeProvider","CacheProvider"]);function I(e){return!!(J.has(e)||e.startsWith("Styled(")&&/^Styled\([a-z][a-zA-Z0-9-]*\)$/.test(e)||e==="lensmcp-root"||e.startsWith("Lensmcp"))}u(I,"R"),m(I,"isInternalRender");let v=!1,y=!1;const Q={};function k(e){const t=e.type;if(!(t==null||typeof t=="string"))switch(e.tag){case U:case Z:return t.displayName||t.name||void 0;case q:return t.displayName||t.render?.displayName||t.render?.name||void 0;case E:case X:return t.displayName||t.type?.displayName||t.type?.name||t.name||void 0;default:return}}u(k,"O"),m(k,"componentNameOf");function z(e){const t=e.alternate?.memoizedProps,s=e.memoizedProps;if(!t||!s||t===s)return[];const r=[];for(const o of Object.keys(s))if(o!=="children"&&!Object.is(t[o],s[o])&&(r.push(o),r.length>=Y))break;return r}u(z,"H"),m(z,"changedPropKeys");const w=new WeakMap;function F(e,t){const s=w.get(e);if(s)return s;const r=e.alternate,o=(r?w.get(r):void 0)??$(t).instanceId;return w.set(e,o),r&&w.set(r,o),o}u(F,"Y"),m(F,"stableInstanceId");const M=m(e=>e.alternate==null||e.child!==e.alternate.child,"childrenChanged");function O(e){const t=[];let s=0,r=!1,o,l=!1;const a=[],i=m((n,d,p)=>{for(let h=n.child;h;h=h.sibling)a.push({node:h,parentName:d,...p?{wrapperName:p}:{}})},"pushChildren");for(M(e.current)&&i(e.current,void 0);a.length>0;){const{node:n,parentName:d,wrapperName:p}=a.pop();if(s+=1,s>P){r=!0;break}let h=d;const g=n.memoizedProps?K(n.memoizedProps):void 0;if(g&&g.length>=(o?.length??0)&&(o=g),n.tag===H&&n.memoizedState!=null&&(l=!0),n.tag===E){M(n)&&i(n,d,k(n));continue}const c=k(n)??p,L=(n.flags&B)!==0;if(c&&L){const C=n.alternate==null,b=[];if(!C){const S=z(n);S.length>0&&b.push({type:"props",changedKeys:S}),d&&b.push({type:"parent-render",parentRenderId:d}),b.length===0&&b.push({type:"react-state",hookIndex:-1})}t.push({name:c,instanceId:F(n,`react:component:(fiber):${c}`),phase:C?"mount":"update",durationMs:typeof n.actualDuration=="number"?n.actualDuration:0,why:b,parentName:d}),h=c}M(n)&&i(n,h)}const f=o?G(o):void 0;return{renders:t,cut:r,suspense:l,...f?{pattern:f}:{}}}u(O,"Z"),m(O,"collectCommitRenders");let T;export function activeCommitId(){return T}u(activeCommitId,"activeCommitId"),m(activeCommitId,"activeCommitId");export function fiberRendersActive(){return v&&!y}u(fiberRendersActive,"fiberRendersActive"),m(fiberRendersActive,"fiberRendersActive");function x(e,t,s=!1){const r=R?e:e.filter(a=>!I(a.name));if(r.length===0)return;const o=$("react:commit").instanceId;T=o;const l=r.length>D?[...r].sort((a,i)=>i.durationMs-a.durationMs).slice(0,D):r;for(const a of l){const i=`react:component:(fiber):${a.name}`,{instanceId:f}=$(i),n={id:f,commitId:o,componentInstanceId:a.instanceId,componentLogicalId:i,componentName:a.name,rendererId:"fiber",phase:a.phase,actualDurationMs:a.durationMs,baseDurationMs:a.durationMs,startTime:t,commitTime:t,why:a.why};N({source:"react",category:"render",severity:"info",title:`render ${a.name} (${a.phase})`,fingerprint:`react-render:${i}`,raw:{kind:"render",render:n}})}if(r.length>l.length||s){const a=s?`at least ${r.length}`:String(r.length);N({source:"react",category:"render",severity:"debug",title:s?`render commit truncated: the walk stopped at ${P} fibers \u2014 ${a} renders, ${l.length} recorded`:`render commit truncated: ${r.length-l.length} of ${r.length} renders dropped (per-commit cap)`,fingerprint:"react-render:commit-truncated",raw:{kind:"render-commit-truncated",commitId:o,total:r.length,published:l.length,...s?{walkStoppedAt:P}:{}}})}}u(x,"q"),m(x,"publishRecords");export function installFiberRenderPublisher(){if(v||y||typeof window>"u")return;const e=window.__LENSMCP_FIBER__;if(!e||typeof e.subscribe!="function")return;v=!0;let t=_,s=0;const r=new Map;let o=0,l;const a=m(()=>{if(l=void 0,t=_,s===0)return;const i=[...r.entries()].sort((d,p)=>p[1]-d[1]).slice(0,5),f=i.length>0?`top: ${i.map(([d,p])=>`${d}\xD7${p}`).join(", ")}`:"no product component in the commits \u2014 all framework-internal",n=o>0?` (+${o} framework-internal)`:"";N({source:"react",category:"render",severity:"warning",title:`render storm: ${s} commits/s over budget \u2014 ${f}${n}`,fingerprint:"react-render:storm",raw:{kind:"render-storm",commits:s,components:Object.fromEntries(i),internalRenders:o}}),s=0,o=0,r.clear()},"flushStorm");e.subscribe((i,f)=>{if(!y)try{if(!f||!f.current)return;const{renders:n,cut:d,pattern:p,suspense:h}=O(f),g=R?n:n.filter(c=>!I(c.name));if(W(Q,{at:Date.now(),renders:g.map(c=>({name:c.name,ms:c.durationMs,mount:c.phase==="mount"})),storm:t<=0,suspense:h,...p?{pattern:p}:{}}),n.length===0)return;if(t>0)t-=1,x(n,Date.now(),d);else{s+=1;for(const c of n){if(!R&&I(c.name)){o+=1;continue}r.set(c.name,(r.get(c.name)??0)+1)}}l||(l=setTimeout(a,1e3))}catch{y=!0}})}u(installFiberRenderPublisher,"installFiberRenderPublisher"),m(installFiberRenderPublisher,"installFiberRenderPublisher");export function resetFiberRenderPublisherForTests(){v=!1,y=!1}u(resetFiberRenderPublisherForTests,"resetFiberRenderPublisherForTests"),m(resetFiberRenderPublisherForTests,"resetFiberRenderPublisherForTests");
|
package/lib/flow-fetch.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var q=Object.defineProperty;var c=(t,r)=>q(t,"name",{value:r,configurable:!0});var j=Object.defineProperty,f=c((t,r)=>j(t,"name",{value:r,configurable:!0}),"d");import{beginBackgroundFlow as G,extendFlowWindow as E,flowForNewActivity as M,publish as $}from"./publish.js";import{installRouteFlows as C}from"./route-flows.js";import{notePageFetch as b,pageVisitOpen as D,startHardPageVisit as U}from"./page-visit-runtime.js";export function mayPropagateTo(t,r=[]){const g=typeof location<"u"?location.href:void 0;let d;try{d=new URL(t,g)}catch{return!1}if(typeof location<"u"&&d.origin===location.origin||H(d.hostname))return!0;for(const n of r){let i;try{i=new URL(n).origin}catch{continue}if(i===d.origin)return!0}return!1}c(mayPropagateTo,"mayPropagateTo"),f(mayPropagateTo,"mayPropagateTo");const W=new Set(["localhost","[::1]","::1"]);function H(t){const r=t.toLowerCase();return W.has(r)||/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(r)}c(H,"j"),f(H,"isLoopbackHost");function F(){const t=globalThis.__LENSMCP_FLOW_PROPAGATE__;return Array.isArray(t)?t.filter(r=>typeof r=="string"):typeof t=="string"?t.split(",").map(r=>r.trim()).filter(Boolean):[]}c(F,"k"),f(F,"globalPropagateList");function x(){return Date.now().toString(36)+Math.random().toString(36).slice(2,12)}c(x,"O"),f(x,"newRequestId");function w(t){return typeof t=="string"?t:t instanceof URL?t.href:t.url??String(t)}c(w,"w"),f(w,"urlOf");const N=Symbol.for("lensmcp.flow-fetch"),S=Symbol.for("lensmcp.flow-fetch.recorded-leg");function m(t){return{...t,[S]:!0}}c(m,"T"),f(m,"markRecorded");let R=!1;export function installFlowFetch(t={}){const r=globalThis;if(typeof r.fetch!="function")return()=>{};if(r.fetch[N])return()=>{};if(!R&&typeof location<"u"&&typeof document<"u"){R=!0;const e=G({originType:"page-load",originNodeId:`page:${location.pathname}`});U(e.flowId)}const g=t.flowHeader??"x-lensmcp-flow-id",d=t.originHeader??"x-lensmcp-origin-node-id",n=t.requestHeader??"x-request-id",i=t.propagateTo??[],s=r.fetch.bind(globalThis);globalThis.__LENSMCP_FLOW_FETCH__=!0;const a=f(((e,o)=>{if(o?.[S])return s(e,o);const l=M();if(!l?.flowId){if(!D())return s(e,o);const h=w(e);if(!mayPropagateTo(h,[...i,...F()])){const P=s(e,m(o));return b(P,y(e,o),h,void 0,void 0),P}const T=new Headers(o?.headers??(typeof e=="object"&&e!==null&&"headers"in e?e.headers:void 0)??void 0),L=T.get(n)??x();T.has(n)||T.set(n,L);const _=s(e,{...m(o),headers:T});return b(_,y(e,o),h,L,void 0),_}if(!mayPropagateTo(w(e),[...i,...F()])){const h=s(e,m(o));return k(l,e,o,h,void 0),b(h,y(e,o),w(e),void 0,l.flowId),h}const O=typeof e=="object"&&e!==null&&"headers"in e?e.headers:void 0,u=new Headers(o?.headers??O??void 0);u.has(g)||u.set(g,l.flowId),l.originNodeId&&!u.has(d)&&u.set(d,l.originNodeId);const v=u.get(n)??x();u.has(n)||u.set(n,v);const I=s(e,{...m(o),headers:u});return k(l,e,o,I,v),b(I,y(e,o),w(e),v,l.flowId),I}),"patched");a[N]=!0,r.fetch=a;const p=t.routeFlows===!1?()=>{}:C();return()=>{p(),r.fetch=s}}c(installFlowFetch,"installFlowFetch"),f(installFlowFetch,"installFlowFetch");function y(t,r){return r?.method??(typeof t=="object"&&t!==null&&"method"in t?t.method??"GET":"GET")}c(y,"b"),f(y,"methodOf");function A(t){const r=typeof location<"u"?location.href:void 0;try{return new URL(t,r).href}catch{return}}c(A,"U"),f(A,"absoluteHref");function k(t,r,g,d,n){const i=w(r),s=A(i),a=g?.method??(typeof r=="object"&&r!==null&&"method"in r?r.method??"GET":"GET"),p=Date.now(),e={flowId:t.flowId,...t.originType?{originType:t.originType}:{},...t.originNodeId?{originNodeId:t.originNodeId}:{},...n?{requestId:n}:{}};$({source:"react",category:"network",severity:"info",title:`fetch ${a} ${i}`,fingerprint:`flow-fetch:${a}:${i}`,context:e,raw:{kind:"fetch-start",url:i,method:a,...s?{href:s}:{},...n?{requestId:n}:{}}}),d.then(o=>{E(t),$({source:"react",category:"network",severity:o.ok||o.status===304?"info":"error",title:`fetch ${a} ${i} \u2192 ${o.status}${o.statusText?` ${o.statusText}`:""}`,fingerprint:`flow-fetch:${a}:${i}:${o.status}`,context:e,raw:{kind:"fetch-done",url:i,...s?{href:s}:{},...n?{requestId:n}:{},method:a,status:o.status,statusText:o.statusText,ok:o.ok,startedAt:p,durationMs:Date.now()-p}})},o=>{E(t),$({source:"react",category:"network",severity:"error",title:`fetch ${a} ${i} failed`,message:o instanceof Error?o.message:String(o),fingerprint:`flow-fetch:${a}:${i}:error`,context:e,raw:{kind:"fetch-error",url:i,...s?{href:s}:{},...n?{requestId:n}:{},method:a,startedAt:p,durationMs:Date.now()-p}})})}c(k,"P"),f(k,"publishFetchLeg");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's own timings for a page visit (planning/pages/00-plan.md S2): Web Vitals, long tasks, the JS heap and
|
|
3
|
+
* the scripts loaded — windowed per visit. Observed from the app's entry module with `buffered: true`, so what
|
|
4
|
+
* happened before it ran (the first paint, the first layout shifts) is still seen; nothing in the page is changed.
|
|
5
|
+
*
|
|
6
|
+
* On a Vite dev server these are the DEV page's timings — every module its own request — not a production build's;
|
|
7
|
+
* the page review says so. TTFB, FCP and LCP belong to a hard load only; CLS, INP and long tasks to any visit.
|
|
8
|
+
*/
|
|
9
|
+
import type { PageVisitRaw } from '@lensmcp/protocol-types';
|
|
10
|
+
type PerfOut = Pick<PageVisitRaw, 'vitals' | 'longTasks' | 'heapMb' | 'chunks'>;
|
|
11
|
+
/** What a visit window asks: its start, the end of what is measured (ready, or the end), and whether it is a hard load. */
|
|
12
|
+
export interface PerfWindow {
|
|
13
|
+
startAt: number;
|
|
14
|
+
toAt: number;
|
|
15
|
+
hard: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** The slice of the browser this needs — a test (or a host without it) gives its own. */
|
|
18
|
+
export interface PerfHost {
|
|
19
|
+
timeOrigin: number;
|
|
20
|
+
observe(type: string, fn: (entries: ReadonlyArray<Record<string, unknown>>) => void): boolean;
|
|
21
|
+
navigation(): Record<string, unknown> | undefined;
|
|
22
|
+
paint(name: string): number | undefined;
|
|
23
|
+
heapBytes(): number | undefined;
|
|
24
|
+
}
|
|
25
|
+
export declare class PagePerf {
|
|
26
|
+
private readonly host;
|
|
27
|
+
private marks;
|
|
28
|
+
private scripts;
|
|
29
|
+
/** Long animation frames are the better long task: they include rendering. Seen at all, `longtask` is not kept. */
|
|
30
|
+
private loaf;
|
|
31
|
+
constructor(host: PerfHost);
|
|
32
|
+
/** What the browser measured within one visit's window. */
|
|
33
|
+
window(w: PerfWindow): PerfOut;
|
|
34
|
+
private push;
|
|
35
|
+
}
|
|
36
|
+
/** The browser as a {@link PerfHost}, or undefined where there is no PerformanceObserver. */
|
|
37
|
+
export declare function browserPerfHost(): PerfHost | undefined;
|
|
38
|
+
export {};
|
package/lib/page-perf.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var k=Object.defineProperty;var c=(o,e)=>k(o,"name",{value:e,configurable:!0});var E=Object.defineProperty,a=c((o,e)=>E(o,"name",{value:e,configurable:!0}),"a");const T=600,M=3e3,r=a(o=>typeof o=="number"&&Number.isFinite(o)?o:0,"num"),h=a(o=>Math.round(o*10)/10,"r1");export class PagePerf{static{c(this,"PagePerf")}static{a(this,"PagePerf")}constructor(e){this.host=e,this.marks=[],this.scripts=[],this.loaf=!1;const i=a(s=>e.timeOrigin+r(s.startTime),"at");e.observe("largest-contentful-paint",s=>{for(const t of s)this.push({type:"lcp",at:i(t),dur:0,value:r(t.startTime)})}),e.observe("layout-shift",s=>{for(const t of s)t.hadRecentInput!==!0&&this.push({type:"shift",at:i(t),dur:0,value:r(t.value)})}),e.observe("event",s=>{for(const t of s)r(t.interactionId)>0&&this.push({type:"event",at:i(t),dur:r(t.duration),value:0})}),this.loaf=e.observe("long-animation-frame",s=>{for(const t of s)this.push({type:"long",at:i(t),dur:r(t.duration),value:0})}),this.loaf||e.observe("longtask",s=>{for(const t of s)this.push({type:"long",at:i(t),dur:r(t.duration),value:0})}),e.observe("resource",s=>{for(const t of s)t.initiatorType!=="script"&&!/\.(m?js|tsx?|jsx)(\?|$)/.test(String(t.name??""))||(this.scripts.push({at:i(t),kb:(r(t.transferSize)||r(t.encodedBodySize))/1024}),this.scripts.length>3e3&&this.scripts.splice(0,this.scripts.length-3e3))})}window(e){const i=a(n=>n.at>=e.startAt&&n.at<=e.toAt,"inside"),s={};if(e.hard){const n=this.host.navigation();n&&(r(n.responseStart)>0&&(s.ttfb=h(r(n.responseStart))),r(n.domContentLoadedEventEnd)>0&&(s.dcl=h(r(n.domContentLoadedEventEnd))),r(n.loadEventEnd)>0&&(s.load=h(r(n.loadEventEnd))));const g=this.host.paint("first-contentful-paint");g!==void 0&&(s.fcp=h(g));const y=this.marks.filter(b=>b.type==="lcp"&&b.at<=e.toAt).at(-1);y&&(s.lcp=h(y.value))}let t=0,p=0,u=0,d=0,l=0;for(const n of this.marks)i(n)&&(n.type==="shift"?t+=n.value:n.type==="event"?p=Math.max(p,n.dur):n.type==="long"&&(u++,d+=n.dur,l=Math.max(l,n.dur)));t>0&&(s.cls=Math.round(t*1e3)/1e3),p>0&&(s.inp=h(p));let f=0,v=0;for(const n of this.scripts)n.at<e.startAt||n.at>e.toAt||(f++,v+=n.kb);const m=this.host.heapBytes();return{...Object.keys(s).length?{vitals:s}:{},...u?{longTasks:{n:u,totalMs:h(d),maxMs:h(l)}}:{},...m!==void 0?{heapMb:h(m/(1<<20))}:{},...f?{chunks:{n:f,kb:Math.round(v)}}:{}}}push(e){this.marks.push(e),this.marks.length>600&&this.marks.splice(0,this.marks.length-600)}}export function browserPerfHost(){const o=globalThis,e=o.performance,i=o.PerformanceObserver;if(!e||!i||typeof e.timeOrigin!="number")return;const s=new Set(i.supportedEntryTypes??[]);return{timeOrigin:e.timeOrigin,observe(t,p){if(!s.has(t))return!1;try{return new i(u=>p(u.getEntries())).observe({type:t,buffered:!0,...t==="event"?{durationThreshold:16}:{}}),!0}catch{return!1}},navigation:a(()=>e.getEntriesByType?.("navigation")?.[0]?.toJSON?.(),"navigation"),paint:a(t=>e.getEntriesByName?.(t,"paint")?.[0]?.startTime,"paint"),heapBytes:a(()=>e.memory?.usedJSHeapSize,"heapBytes")}}c(browserPerfHost,"browserPerfHost"),a(browserPerfHost,"browserPerfHost");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PageCommit } from './page-visits.js';
|
|
2
|
+
/** The hard load: from navigation start, once per document. `flowId`: the page-load flow. */
|
|
3
|
+
export declare function startHardPageVisit(flowId?: string): void;
|
|
4
|
+
/** A pathname change through the history API (after it happened). */
|
|
5
|
+
export declare function notePageNavigation(path: string, how: 'push' | 'replace' | 'pop', flowId?: string): void;
|
|
6
|
+
/** Whether a visit is open: a fetch outside any flow then still gets a request id, so the lens can join it. */
|
|
7
|
+
export declare function pageVisitOpen(): boolean;
|
|
8
|
+
/** A fetch the page started; the result's settle (headers or failure) is observed on a branch. */
|
|
9
|
+
export declare function notePageFetch(result: Promise<{
|
|
10
|
+
status: number;
|
|
11
|
+
}>, method: string, url: string, rid: string | undefined, flowId: string | undefined): void;
|
|
12
|
+
/** A commit, from the fiber subscriber that claimed them (`owner`: that module copy's token). */
|
|
13
|
+
export declare function notePageCommit(owner: object, c: PageCommit): void;
|
|
14
|
+
/** A store path updated (valtio-instrumentation calls this before its own, flood-guarded, event). */
|
|
15
|
+
export declare function notePageState(store: string, path: string): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var u=Object.defineProperty;var i=(t,e)=>u(t,"name",{value:e,configurable:!0});var h=Object.defineProperty,n=i((t,e)=>h(t,"name",{value:e,configurable:!0}),"a");import{publish as y}from"./publish.js";import{PageVisitTracker as _}from"./page-visits.js";import{browserPerfHost as l,PagePerf as P}from"./page-perf.js";const p="__LENSMCP_PAGE_VISITS__";function f(){return typeof window<"u"&&typeof location<"u"&&typeof document<"u"}i(f,"y"),n(f,"browser");function d(){if(!f())return;const t=globalThis,e=t[p];if(e===!1)return;if(e)return e;const a=typeof t.__LENSMCP_APP__=="string"&&t.__LENSMCP_APP__?t.__LENSMCP_APP__:location.host||"app";let c;try{const r=l();c=r?new P(r):void 0}catch{c=void 0}const o={tracker:new _({app:a,...c?{perf:n(r=>c.window(r),"perf")}:{},emit:n((r,s)=>y({source:"react",category:"performance",severity:r.readyBy==="timeout"?"warning":"info",title:s,fingerprint:`page-visit:${r.visitId}:${r.phase}`,detached:!0,raw:r}),"emit"),tabId:n(()=>{try{return sessionStorage.getItem("lensmcp:tabId")??void 0}catch{return}},"tabId"),base:n(()=>location.href,"base")}),hardStarted:!1};t[p]=o;try{window.addEventListener("beforeunload",()=>o.tracker.pagehide()),window.addEventListener("pagehide",()=>o.tracker.pagehide()),window.addEventListener("error",r=>o.tracker.error(r.message||"error")),window.addEventListener("unhandledrejection",r=>{const s=r.reason;o.tracker.error(s instanceof Error?`${s.name}: ${s.message}`:String(s))})}catch{}return o}i(d,"f"),n(d,"shared");export function startHardPageVisit(t){const e=d();if(!(!e||e.hardStarted)){e.hardStarted=!0;try{const a=Date.now(),c=typeof performance<"u"&&typeof performance.timeOrigin=="number"?Math.round(performance.timeOrigin):a,o=(typeof performance<"u"&&typeof performance.getEntriesByType=="function"?performance.getEntriesByType("navigation")[0]:void 0)?.type,r=o==="navigate"||o==="reload"||o==="back_forward"?o:void 0;e.tracker.hard({path:location.pathname,startAt:Math.min(c,a),appStartAt:a,...r?{navType:r}:{},...g()?{cause:"hmr"}:{},...t?{flowId:t}:{}})}catch{}}}i(startHardPageVisit,"startHardPageVisit"),n(startHardPageVisit,"startHardPageVisit");function g(){if(globalThis.__LENSMCP_RELOAD_CAUSE__)return!0;try{return!!sessionStorage.getItem("lensmcp:reloadCause")}catch{return!1}}i(g,"_"),n(g,"hmrReload");export function notePageNavigation(t,e,a){try{d()?.tracker.navigate({path:t,how:e,...a?{flowId:a}:{}})}catch{}}i(notePageNavigation,"notePageNavigation"),n(notePageNavigation,"notePageNavigation");export function pageVisitOpen(){const t=globalThis[p];return!!t&&t.tracker.open()}i(pageVisitOpen,"pageVisitOpen"),n(pageVisitOpen,"pageVisitOpen");export function notePageFetch(t,e,a,c,o){const r=d();if(r)try{const s=r.tracker.fetchStarted({method:e,url:a,...c?{rid:c}:{},...o?{flowId:o}:{}});t.then(m=>s(m.status),()=>s(0))}catch{}}i(notePageFetch,"notePageFetch"),n(notePageFetch,"notePageFetch");export function notePageCommit(t,e){const a=d();if(a&&(a.commitsBy??=t,a.commitsBy===t))try{a.tracker.commit(e)}catch{}}i(notePageCommit,"notePageCommit"),n(notePageCommit,"notePageCommit");export function notePageState(t,e){const a=globalThis[p];if(a)try{a.tracker.storeUpdated(t,e)}catch{}}i(notePageState,"notePageState"),n(notePageState,"notePageState");
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page visits — what the browser measures of each page it shows (planning/pages/00-plan.md §2).
|
|
3
|
+
*
|
|
4
|
+
* A visit starts at navigation start (a hard load) or at a pathname change (an SPA navigation), is READY at the first
|
|
5
|
+
* commit after the page's own requests settle (decision 8: no `markReady()`), and ends at the next visit, `pagehide`,
|
|
6
|
+
* or a minute after ready. It is not a flow: on FoodGuard a navigation landed inside a click's flow still open 29 s
|
|
7
|
+
* after the click, and most of a page's renders and store updates never reach the bus — so the lens cannot cut a
|
|
8
|
+
* visit out of its flows. The browser counts it as it happens and says so twice: a `page-visit` event at ready, one at
|
|
9
|
+
* the end (`@lensmcp/protocol-types` `PageVisitRaw`); the lens joins its requests to the backend by request id.
|
|
10
|
+
*
|
|
11
|
+
* The ready rule, on what the browser sees:
|
|
12
|
+
* - a page's OWN requests are every `fetch` it starts between the visit's start and ready, in a flow or not; one
|
|
13
|
+
* settles when its response headers arrive (a streamed body does not hold ready);
|
|
14
|
+
* - a commit is ready when none of them is in flight, no Suspense boundary shows its fallback, and no new fetch
|
|
15
|
+
* starts within {@link QUIET_MS} after it — a page's first commit is its skeleton, and its effects start the
|
|
16
|
+
* fetches 1–5 ms later (tetros, measured); a serial chain keeps waiting the same way;
|
|
17
|
+
* - no requests at all: the first commit that holds the quiet window (`no-requests`); requests settled and nothing
|
|
18
|
+
* committed within {@link NO_COMMIT_MS}: the last commit before (`no-commit`); still not ready after
|
|
19
|
+
* {@link READY_CAP_MS}: `timeout`, its open requests named; the next visit came first: `left`.
|
|
20
|
+
*
|
|
21
|
+
* Pure: the clock, the timers and where the events go are given. `installPageVisits` wires it to the browser.
|
|
22
|
+
*/
|
|
23
|
+
import type { PageVisitNavType, PageVisitRaw, PageVisitReadyBy, PageVisitRequest } from '@lensmcp/protocol-types';
|
|
24
|
+
import type { PerfWindow } from './page-perf.js';
|
|
25
|
+
export declare const QUIET_MS = 100;
|
|
26
|
+
export declare const READY_CAP_MS = 30000;
|
|
27
|
+
export declare const NO_COMMIT_MS = 1000;
|
|
28
|
+
export declare const END_AFTER_READY_MS = 60000;
|
|
29
|
+
export interface VisitClock {
|
|
30
|
+
now(): number;
|
|
31
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
32
|
+
clearTimeout(handle: unknown): void;
|
|
33
|
+
}
|
|
34
|
+
/** One commit as the fiber walk saw it — every commit, a storm's too. */
|
|
35
|
+
export interface PageCommit {
|
|
36
|
+
at: number;
|
|
37
|
+
/** The product components it rendered (framework internals left out): name, React's render ms, mounted. */
|
|
38
|
+
renders: ReadonlyArray<{
|
|
39
|
+
name: string;
|
|
40
|
+
ms: number;
|
|
41
|
+
mount: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
/** Over the render publisher's rate budget: folded into a storm, not published. */
|
|
44
|
+
storm: boolean;
|
|
45
|
+
/** A Suspense boundary in the committed tree shows its fallback. */
|
|
46
|
+
suspense: boolean;
|
|
47
|
+
/** The router's full match pattern, when this commit rendered a route. */
|
|
48
|
+
pattern?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface PageVisitTrackerOptions {
|
|
51
|
+
/** The app the Vite plugin serves, or the page's host. */
|
|
52
|
+
app: string;
|
|
53
|
+
clock?: VisitClock;
|
|
54
|
+
/** Where a visit's events go (`publish`). */
|
|
55
|
+
emit: (raw: PageVisitRaw, title: string) => void;
|
|
56
|
+
tabId?: () => string | undefined;
|
|
57
|
+
/** Resolves a request URL for its host and path (the page's location in a browser). */
|
|
58
|
+
base?: () => string | undefined;
|
|
59
|
+
/** The browser's own timings within a window of the visit (page-perf.ts): vitals, long tasks, heap, scripts. */
|
|
60
|
+
perf?: (w: PerfWindow) => Pick<PageVisitRaw, 'vitals' | 'longTasks' | 'heapMb' | 'chunks'>;
|
|
61
|
+
}
|
|
62
|
+
interface RenderTally {
|
|
63
|
+
commits: number;
|
|
64
|
+
renders: number;
|
|
65
|
+
renderMs: number;
|
|
66
|
+
maxCommitMs: number;
|
|
67
|
+
stormCommits: number;
|
|
68
|
+
mounts: number;
|
|
69
|
+
suspenseWaits: number;
|
|
70
|
+
byName: Map<string, {
|
|
71
|
+
n: number;
|
|
72
|
+
ms: number;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
interface Visit {
|
|
76
|
+
id: string;
|
|
77
|
+
nav: 'hard' | 'spa';
|
|
78
|
+
navType?: PageVisitNavType;
|
|
79
|
+
cause?: 'hmr';
|
|
80
|
+
path: string;
|
|
81
|
+
redirectedFrom?: string;
|
|
82
|
+
startAt: number;
|
|
83
|
+
appStartAt?: number;
|
|
84
|
+
pattern?: string;
|
|
85
|
+
flowIds: Set<string>;
|
|
86
|
+
requests: PageVisitRequest[];
|
|
87
|
+
dropped: number;
|
|
88
|
+
/** Own requests in flight (before ready). */
|
|
89
|
+
open: number;
|
|
90
|
+
r: RenderTally;
|
|
91
|
+
updates: number;
|
|
92
|
+
byStore: Map<string, {
|
|
93
|
+
store: string;
|
|
94
|
+
path: string;
|
|
95
|
+
n: number;
|
|
96
|
+
}>;
|
|
97
|
+
errors: number;
|
|
98
|
+
firstError?: string;
|
|
99
|
+
lastCommitAt?: number;
|
|
100
|
+
/** The commit waiting out the quiet window. */
|
|
101
|
+
candidate?: number;
|
|
102
|
+
readyAt?: number;
|
|
103
|
+
readyBy?: PageVisitReadyBy;
|
|
104
|
+
after: {
|
|
105
|
+
requests: number;
|
|
106
|
+
templates: Set<string>;
|
|
107
|
+
renders: number;
|
|
108
|
+
updates: number;
|
|
109
|
+
};
|
|
110
|
+
quiet?: unknown;
|
|
111
|
+
cap?: unknown;
|
|
112
|
+
noCommit?: unknown;
|
|
113
|
+
end?: unknown;
|
|
114
|
+
}
|
|
115
|
+
/** A path with its ids folded: `/orders/42` → `/orders/:id` — what tells polling from browsing after ready. */
|
|
116
|
+
export declare function templatePath(path: string): string;
|
|
117
|
+
export declare class PageVisitTracker {
|
|
118
|
+
private readonly opts;
|
|
119
|
+
private readonly clock;
|
|
120
|
+
private visit;
|
|
121
|
+
constructor(opts: PageVisitTrackerOptions);
|
|
122
|
+
/** Test seam: the visit open now. */
|
|
123
|
+
current(): Readonly<Visit> | undefined;
|
|
124
|
+
/** Whether a visit is open — a fetch made outside any flow then still gets a request id, to be joined. */
|
|
125
|
+
open(): boolean;
|
|
126
|
+
/** A hard load: from navigation start; the app's own start is when its entry module ran. */
|
|
127
|
+
hard(o: {
|
|
128
|
+
path: string;
|
|
129
|
+
startAt: number;
|
|
130
|
+
appStartAt: number;
|
|
131
|
+
navType?: PageVisitNavType;
|
|
132
|
+
cause?: 'hmr';
|
|
133
|
+
flowId?: string;
|
|
134
|
+
}): void;
|
|
135
|
+
/** A pathname change through the history API. A `replace` before ready is a redirect: the visit goes on. */
|
|
136
|
+
navigate(o: {
|
|
137
|
+
path: string;
|
|
138
|
+
how: 'push' | 'replace' | 'pop';
|
|
139
|
+
flowId?: string;
|
|
140
|
+
}): void;
|
|
141
|
+
/** A fetch the page started; call the returned function when its response headers arrive (status 0: it failed). */
|
|
142
|
+
fetchStarted(o: {
|
|
143
|
+
rid?: string;
|
|
144
|
+
method: string;
|
|
145
|
+
url: string;
|
|
146
|
+
flowId?: string;
|
|
147
|
+
}): (status: number) => void;
|
|
148
|
+
commit(c: PageCommit): void;
|
|
149
|
+
/** A store path updated (valtio). */
|
|
150
|
+
storeUpdated(store: string, path: string): void;
|
|
151
|
+
error(message: string): void;
|
|
152
|
+
/** The tab is going away: the visit ends now. */
|
|
153
|
+
pagehide(): void;
|
|
154
|
+
private begin;
|
|
155
|
+
private ready;
|
|
156
|
+
/** The visit ends: a `ready` first if it never got there (`left`), then its `end`. */
|
|
157
|
+
private finish;
|
|
158
|
+
private send;
|
|
159
|
+
private where;
|
|
160
|
+
private cancelCandidate;
|
|
161
|
+
private clearTimer;
|
|
162
|
+
}
|
|
163
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var v=Object.defineProperty;var l=(a,e)=>v(a,"name",{value:e,configurable:!0});var f=Object.defineProperty,d=l((a,e)=>f(a,"name",{value:e,configurable:!0}),"o");export const QUIET_MS=100,READY_CAP_MS=3e4,NO_COMMIT_MS=1e3,END_AFTER_READY_MS=6e4;const g=60,C=200,I=8,S=20,y={now:d(()=>Date.now(),"now"),setTimeout:d((a,e)=>setTimeout(a,e),"setTimeout"),clearTimeout:d(a=>clearTimeout(a),"clearTimeout")},c=d((a,e=200)=>a.length>e?a.slice(0,e):a,"clip");function u(a){let e=2166136261;for(let t=0;t<a.length;t++)e^=a.charCodeAt(t),e=Math.imul(e,16777619);return(e>>>0).toString(36)}l(u,"l"),d(u,"shortHash");export function templatePath(a){return a.split("/").map(e=>/^\d+$/.test(e)||/^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(e)||/^[0-9a-zA-Z_-]{20,}$/.test(e)?":id":e).join("/")}l(templatePath,"templatePath"),d(templatePath,"templatePath");const A=d(()=>({commits:0,renders:0,renderMs:0,maxCommitMs:0,stormCommits:0,mounts:0,suspenseWaits:0,byName:new Map}),"newTally");let w=0;const T=d(a=>`pv-${a.toString(36)}-${(++w).toString(36)}${Math.random().toString(36).slice(2,6)}`,"newVisitId");export class PageVisitTracker{static{l(this,"PageVisitTracker")}static{d(this,"PageVisitTracker")}constructor(e){this.opts=e,this.clock=e.clock??y}current(){return this.visit}open(){return this.visit!==void 0}hard(e){this.begin({nav:"hard",path:e.path,startAt:e.startAt,appStartAt:e.appStartAt,...e.navType?{navType:e.navType}:{},...e.cause?{cause:e.cause}:{},...e.flowId?{flowId:e.flowId}:{}})}navigate(e){const t=this.visit;if(!(t&&c(e.path)===t.path)){if(t&&e.how==="replace"&&t.readyAt===void 0){t.redirectedFrom??=t.path,t.path=c(e.path),t.pattern=void 0,e.flowId&&t.flowIds.add(e.flowId);return}this.begin({nav:"spa",path:e.path,startAt:this.clock.now(),navType:e.how,...e.flowId?{flowId:e.flowId}:{}})}}fetchStarted(e){const t=this.visit;if(!t)return()=>{};const s=this.clock.now();e.flowId&&t.flowIds.add(e.flowId);const o=this.where(e.url),r=t.readyAt!==void 0;let i;if(t.requests.length<60?(i={rid:e.rid??"",m:e.method.toUpperCase(),p:o.p,...o.qh?{qh:o.qh}:{},s:Math.max(0,s-t.startAt),...e.flowId?{flowId:e.flowId}:{},...r?{late:!0}:{}},t.requests.push(i)):t.dropped++,r)return t.after.requests++,t.after.templates.size<20&&t.after.templates.add(`${e.method.toUpperCase()} ${templatePath(o.path)}`),m=>{i&&(i.d=Math.max(0,this.clock.now()-t.startAt-i.s),i.st=m)};t.open++,this.cancelCandidate(t),this.clearTimer(t,"noCommit");let p=!1;return m=>{p||(p=!0,i&&(i.d=Math.max(0,this.clock.now()-t.startAt-i.s),i.st=m),!(this.visit!==t||t.readyAt!==void 0)&&(t.open=Math.max(0,t.open-1),t.open===0&&(t.noCommit=this.clock.setTimeout(()=>{t.noCommit=void 0,this.visit===t&&t.readyAt===void 0&&t.open===0&&this.ready(t,t.lastCommitAt??this.clock.now(),"no-commit")},1e3))))}}commit(e){const t=this.visit;if(t){if(e.pattern&&t.readyBy===void 0&&(t.pattern=e.pattern),t.readyAt!==void 0){t.after.renders+=e.renders.length;return}if(e.renders.length>0){const s=t.r;s.commits++,s.renders+=e.renders.length;let o=0;for(const r of e.renders){r.ms>o&&(o=r.ms),r.mount&&s.mounts++;const i=s.byName.get(r.name);i?(i.n++,i.ms+=r.ms):s.byName.set(r.name,{n:1,ms:r.ms})}s.renderMs+=o,o>s.maxCommitMs&&(s.maxCommitMs=o),e.storm&&s.stormCommits++}if(e.suspense&&t.r.suspenseWaits++,t.lastCommitAt=e.at,this.clearTimer(t,"noCommit"),t.open>0||e.suspense){this.cancelCandidate(t);return}this.cancelCandidate(t),t.candidate=e.at,t.quiet=this.clock.setTimeout(()=>{t.quiet=void 0,!(this.visit!==t||t.readyAt!==void 0||t.candidate===void 0)&&this.ready(t,t.candidate,t.requests.length?"settled":"no-requests")},100)}}storeUpdated(e,t){const s=this.visit;if(!s)return;if(s.readyAt!==void 0){s.after.updates++;return}s.updates++;const o=`${e}\0${t}`,r=s.byStore.get(o);r?r.n++:s.byStore.size<200&&s.byStore.set(o,{store:e,path:c(t,120),n:1})}error(e){const t=this.visit;t&&(t.errors++,t.firstError??=c(e,200))}pagehide(){this.visit&&this.finish(this.visit)}begin(e){this.visit&&this.finish(this.visit);const t={id:T(e.startAt),nav:e.nav,...e.navType?{navType:e.navType}:{},...e.cause?{cause:e.cause}:{},path:c(e.path),startAt:e.startAt,...e.appStartAt!==void 0?{appStartAt:e.appStartAt}:{},flowIds:new Set(e.flowId?[e.flowId]:[]),requests:[],dropped:0,open:0,r:A(),updates:0,byStore:new Map,errors:0,after:{requests:0,templates:new Set,renders:0,updates:0}};this.visit=t,t.cap=this.clock.setTimeout(()=>{t.cap=void 0,this.visit===t&&t.readyAt===void 0&&this.ready(t,this.clock.now(),"timeout")},Math.max(0,3e4-(this.clock.now()-t.startAt)))}ready(e,t,s){e.readyAt=s==="left"?void 0:t,e.readyBy=s,this.clearTimer(e,"quiet"),this.clearTimer(e,"noCommit"),this.clearTimer(e,"cap"),e.candidate=void 0,this.send(e,"ready"),s!=="left"&&(e.end=this.clock.setTimeout(()=>{e.end=void 0,this.visit===e&&this.finish(e)},6e4))}finish(e){e.readyBy===void 0&&this.ready(e,this.clock.now(),"left");for(const t of["quiet","noCommit","cap","end"])this.clearTimer(e,t);this.send(e,"end"),this.visit===e&&(this.visit=void 0)}send(e,t){const s=e.r,o=this.opts.tabId?.(),r=e.readyAt!==void 0?Math.max(0,e.readyAt-e.startAt):void 0,i={kind:"page-visit",phase:t,visitId:e.id,...o?{tabId:o}:{},app:this.opts.app,...e.pattern?{pattern:e.pattern}:{},path:e.path,nav:e.nav,...e.navType?{navType:e.navType}:{},...e.cause?{cause:e.cause}:{},...e.redirectedFrom?{redirectedFrom:e.redirectedFrom}:{},startAt:e.startAt,...e.readyAt!==void 0?{readyAt:e.readyAt}:{},...e.readyBy?{readyBy:e.readyBy}:{},...r!==void 0?{readyMs:r}:{},...r!==void 0&&e.appStartAt!==void 0?{appReadyMs:Math.max(0,e.readyAt-e.appStartAt)}:{},...t==="end"?{endAt:this.clock.now()}:{},flowIds:[...e.flowIds].slice(0,20),requests:e.requests.map(n=>({...n})),...e.dropped?{requestsDropped:e.dropped}:{},renders:{commits:s.commits,renders:s.renders,renderMs:Math.round(s.renderMs*10)/10,maxCommitMs:Math.round(s.maxCommitMs*10)/10,stormCommits:s.stormCommits,mounts:s.mounts,suspenseWaits:s.suspenseWaits,top:[...s.byName.entries()].sort((n,h)=>h[1].n-n[1].n||h[1].ms-n[1].ms).slice(0,8).map(([n,h])=>({name:n,n:h.n,ms:Math.round(h.ms*10)/10}))},store:{updates:e.updates,top:[...e.byStore.values()].sort((n,h)=>h.n-n.n).slice(0,8)},errors:{n:e.errors,...e.firstError?{first:e.firstError}:{}},...t==="end"?{afterReady:{requests:e.after.requests,templates:[...e.after.templates],renders:e.after.renders,updates:e.after.updates}}:{}};try{const n=t==="ready"?e.readyAt??this.clock.now():this.clock.now();this.opts.perf&&Object.assign(i,this.opts.perf({startAt:e.startAt,toAt:n,hard:e.nav==="hard"}))}catch{}const p=e.pattern??e.path,m=t==="end"?`page visit ended ${p}`:e.readyBy==="left"?`page left before ready ${p}`:e.readyBy==="timeout"?`page not ready in ${3e4/1e3}s ${p} \u2014 ${e.requests.filter(n=>n.d===void 0).length} requests open`:`page ready ${p} \xB7 ${r} ms`;try{this.opts.emit(i,m)}catch{}}where(e){try{const t=new URL(e,this.opts.base?.()??"http://localhost/"),s=t.search.length>1?u(t.search):void 0;return{p:c(`${t.host}${t.pathname}`),path:t.pathname,...s?{qh:s}:{}}}catch{const[t=""]=e.split("?");return{p:c(t),path:t}}}cancelCandidate(e){e.candidate=void 0,this.clearTimer(e,"quiet")}clearTimer(e,t){e[t]!==void 0&&(this.clock.clearTimeout(e[t]),e[t]=void 0)}}
|
package/lib/publish.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactPublishPayload } from './types.js';
|
|
2
|
-
export type PublishCategory = 'render' | 'state' | 'runtime' | 'network' | 'visual' | 'trace';
|
|
2
|
+
export type PublishCategory = 'render' | 'state' | 'runtime' | 'network' | 'visual' | 'trace' | 'performance';
|
|
3
3
|
export interface FlowSpec {
|
|
4
4
|
flowId: string;
|
|
5
5
|
originType?: string;
|
|
@@ -23,6 +23,8 @@ export interface PublishEnvelope {
|
|
|
23
23
|
url: string;
|
|
24
24
|
}>;
|
|
25
25
|
raw: ReactPublishPayload | Record<string, unknown>;
|
|
26
|
+
/** Not any flow's: no flow is stamped on it (a page visit — the flows it ran under travel in its own payload). */
|
|
27
|
+
detached?: true;
|
|
26
28
|
}
|
|
27
29
|
type PublishFn = (event: PublishEnvelope) => void;
|
|
28
30
|
declare global {
|
package/lib/publish.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var O=Object.defineProperty;var t=(e,o)=>O(e,"name",{value:o,configurable:!0});var W=Object.defineProperty,n=t((e,o)=>W(e,"name",{value:o,configurable:!0}),"t");import{createEventFloodGuard as U,estimateJsonBytes as $}from"@lensmcp/protocol-types";const u=[],X=200,M=(()=>{const e=typeof process<"u"?process.env?.LENSMCP_PUBLISH_QUEUE_BYTES:void 0,o=Number(e);return Number.isFinite(o)&&o>0?Math.floor(o):1024*1024})(),I=[];let s=0;function S(e){const o=$(e,M);for(u.push(e),I.push(o),s+=o;u.length>X||s>M&&u.length>1;)u.shift(),s-=I.shift()??0;u.length===0&&(s=0)}t(S,"k"),n(S,"enqueue");function L(){const e=u.shift();return s-=I.shift()??0,u.length===0&&(s=0),e}t(L,"D"),n(L,"dequeue");export function queuedByteCount(){return s}t(queuedByteCount,"queuedByteCount"),n(queuedByteCount,"queuedByteCount");const p=[],F=4e3;let c;export const DEFAULT_AMBIENT_FLOW_MAX_MS=3e4;let y;function N(e){if(e==="off"||e==="Infinity")return 1/0;const o=typeof e=="string"&&e.trim()!==""?Number(e):e;if(!(typeof o!="number"||Number.isNaN(o)||o<0))return o===0||o===1/0?1/0:o}t(N,"N"),n(N,"parseMaxLifetime");function E(){if(y!==void 0)return y;const e=N(globalThis.__LENSMCP_AMBIENT_FLOW_MAX_MS__);return e!==void 0?e:N(typeof process<"u"?process.env?.LENSMCP_AMBIENT_FLOW_MAX_MS:void 0)??DEFAULT_AMBIENT_FLOW_MAX_MS}t(E,"P"),n(E,"ambientMaxMs");export function configureAmbientFlows(e){y=e.maxLifetimeMs===void 0?void 0:N(e.maxLifetimeMs)}t(configureAmbientFlows,"configureAmbientFlows"),n(configureAmbientFlows,"configureAmbientFlows");const d=new Map,j=16;function B(e){for(d.set(e,{bornAt:Date.now()});d.size>j;)d.delete(d.keys().next().value)}t(B,"X"),n(B,"trackAmbient");function b(e){const o=d.get(e.flowId);if(!o)return!1;const i=E();return i!==1/0&&Date.now()-o.bornAt>=i}t(b,"h"),n(b,"ambientExhausted");export function extendFlowWindow(e){const o=e??currentFlow()??c?.spec;o&&(b(o)||(c={spec:o,until:Date.now()+F}))}t(extendFlowWindow,"extendFlowWindow"),n(extendFlowWindow,"extendFlowWindow");function a(){return c&&Date.now()<c.until?c.spec:void 0}t(a,"y"),n(a,"windowedFlow");export function activeFlow(){return currentFlow()??a()}t(activeFlow,"activeFlow"),n(activeFlow,"activeFlow");export function beginBackgroundFlow(e){const o={flowId:h(),originType:e.originType,...e.originNodeId?{originNodeId:e.originNodeId}:{}};return c={spec:o,until:Date.now()+F},B(o.flowId),publish({source:"react",category:"runtime",severity:"info",title:e.originType,fingerprint:_(e.originNodeId??e.originType,o.flowId),raw:{kind:"flow-origin",originNodeId:e.originNodeId,...e.continuesFlowId?{continuesFlowId:e.continuesFlowId}:{}}}),o}t(beginBackgroundFlow,"beginBackgroundFlow"),n(beginBackgroundFlow,"beginBackgroundFlow");export function flowForNewActivity(){const e=currentFlow();if(e)return e;const o=a();if(o)return b(o)?beginBackgroundFlow({originType:"timer",originNodeId:typeof location<"u"?`ambient:${location.pathname}`:"ambient",continuesFlowId:o.flowId}):o}t(flowForNewActivity,"flowForNewActivity"),n(flowForNewActivity,"flowForNewActivity");export const FLOW_ORIGIN_FINGERPRINT_PREFIX="flow-origin:";function _(e,o){return`${FLOW_ORIGIN_FINGERPRINT_PREFIX}${e}:${o}`}t(_,"x"),n(_,"originFingerprint");export function setPublisher(e){if(globalThis.__LENSMCP_PUBLISH__=e,e)for(;u.length;){const o=L();if(o)try{e(o)}catch{}}}t(setPublisher,"setPublisher"),n(setPublisher,"setPublisher");export function runInFlow(e,o){p.push(e),c={spec:e,until:Date.now()+F};try{return o()}finally{p.pop()}}t(runInFlow,"runInFlow"),n(runInFlow,"runInFlow");export function currentFlow(){return p[p.length-1]}t(currentFlow,"currentFlow"),n(currentFlow,"currentFlow");const A=Symbol.for("lensmcp.flowed"),P=2e3,f=new Map;function k(e){if(e==null||typeof e!="object")return;const o=e,i=o.nativeEvent;if(i!=null&&typeof i=="object"){const r=i.isTrusted;if(typeof r=="boolean")return r}if(typeof o.isTrusted=="boolean")return o.isTrusted}t(k,"v"),n(k,"eventTrust");function D(e){if(!(f.size<=64))for(const[o,i]of f)e>=i.until&&f.delete(o)}t(D,"Q"),n(D,"pruneInputBursts");export function withFlow(e,o={}){if(typeof e!="function"||e[A])return e;const i=n(()=>{publish({source:"react",category:"runtime",severity:"info",title:o.originType??"user-click",fingerprint:_(o.originNodeId??"unknown",currentFlow()?.flowId??""),raw:{kind:"flow-origin",originNodeId:o.originNodeId}})},"publishOrigin"),r=n(function(...l){if(o.originType==="input-change"){if(k(l[0])===!1)return currentFlow()==null&&a()!=null&&extendFlowWindow(),e(...l);const v=o.originNodeId??"input",w=Date.now(),g=f.get(v);if(g&&w<g.until)return g.until=w+P,runInFlow(g.spec,()=>e(...l));const x={flowId:h(),originType:"input-change",originNodeId:o.originNodeId};return f.set(v,{spec:x,until:w+P}),D(w),runInFlow(x,()=>(i(),e(...l)))}const C=h();return runInFlow({flowId:C,originType:o.originType??"user-click",originNodeId:o.originNodeId},()=>(i(),e(...l)))},"flowed");return r[A]=!0,r}t(withFlow,"withFlow"),n(withFlow,"withFlow");let m=0;function h(){return m=(m+1)%65535,`flow:${Date.now().toString(36)}:${m.toString(36)}`}t(h,"b"),n(h,"newFlowId");export function publish(e){if(e.detached){const{detached:r,...l}=e;T(l);return}const o=currentFlow()??(e.context?.flowId?void 0:a()),i=o==null?e:{...e,context:{...o.flowId?{flowId:o.flowId}:{},...o.originType?{originType:o.originType}:{},...o.originNodeId?{originNodeId:o.originNodeId}:{},...o.causedByNodeId?{causedByNodeId:o.causedByNodeId}:{},...e.context}};T(i)}t(publish,"publish"),n(publish,"publish");function T(e){const o=globalThis.__LENSMCP_PUBLISH__;if(o)try{o(e);return}catch{}S(e)}t(T,"L"),n(T,"deliver");const q=U({burst:30,perSec:5});export function publishLazy(e,o){const i=e.startsWith(FLOW_ORIGIN_FINGERPRINT_PREFIX)?{ok:!0,suppressed:0}:q.admit(e);if(!i.ok)return;const r=o();if(i.suppressed>0&&r.raw!==null&&typeof r.raw=="object"){publish({...r,raw:{...r.raw,floodSuppressed:i.suppressed}});return}publish(r)}t(publishLazy,"publishLazy"),n(publishLazy,"publishLazy");export function resetFlowStateForTests(){p.length=0,c=void 0,d.clear(),f.clear(),y=void 0}t(resetFlowStateForTests,"resetFlowStateForTests"),n(resetFlowStateForTests,"resetFlowStateForTests");export function drainQueue(){return I.length=0,s=0,u.splice(0,u.length)}t(drainQueue,"drainQueue"),n(drainQueue,"drainQueue");
|
package/lib/route-flows.d.ts
CHANGED
|
@@ -2,8 +2,19 @@
|
|
|
2
2
|
export interface RouteFlowEnvironment {
|
|
3
3
|
history: Pick<History, 'pushState' | 'replaceState'>;
|
|
4
4
|
location: Pick<Location, 'pathname'>;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The client runtime's popstate relay (vite-plugin's inline runtime): its listener was added before the app's
|
|
7
|
+
* router's, so it hears back/forward first. Without it, a listener of our own — after the router's.
|
|
8
|
+
*/
|
|
9
|
+
navBus?: {
|
|
10
|
+
subscribe(fn: () => void): () => void;
|
|
11
|
+
};
|
|
12
|
+
addEventListener(type: 'popstate', listener: () => void, options?: {
|
|
13
|
+
capture?: boolean;
|
|
14
|
+
}): void;
|
|
15
|
+
removeEventListener(type: 'popstate', listener: () => void, options?: {
|
|
16
|
+
capture?: boolean;
|
|
17
|
+
}): void;
|
|
7
18
|
}
|
|
8
19
|
/** The node id of a route flow: `route:/companies/42/staff`. Pathname only, clipped. */
|
|
9
20
|
export declare function routeNodeId(pathname: string): string;
|
package/lib/route-flows.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var w=Object.defineProperty;var r=(e,t)=>w(e,"name",{value:t,configurable:!0});var L=Object.defineProperty,o=r((e,t)=>L(e,"name",{value:t,configurable:!0}),"e");import{activeFlow as S,beginBackgroundFlow as N}from"./publish.js";import{notePageNavigation as _}from"./page-visit-runtime.js";const b=200;function g(){const e=globalThis.window;if(!e?.history||!e.location||typeof e.addEventListener!="function"||typeof e.removeEventListener!="function")return;const t=e.__LENSMCP_NAV__;return t&&typeof t.subscribe=="function"?{history:e.history,location:e.location,navBus:t,addEventListener:e.addEventListener.bind(e),removeEventListener:e.removeEventListener.bind(e)}:e}r(g,"v"),o(g,"browserEnvironment");export function routeNodeId(e){return`route:${e.length>b?e.slice(0,b):e}`}r(routeNodeId,"routeNodeId"),o(routeNodeId,"routeNodeId");let c;export function installRouteFlows(e=g()){if(!e)return()=>{};if(c)return()=>{};let t=e.location.pathname,p=!0;const l=o(u=>{if(!p)return;const n=e.location.pathname;if(n===t)return;t=n;const i=S();_(n,u,i?.flowId),!i&&N({originType:"route-load",originNodeId:routeNodeId(n)})},"navigated"),d=o((u,n)=>o(function(...i){const E=u.apply(this,i);try{l(n)}catch{}return E},"wrapped"),"wrap"),v=e.history.pushState,h=e.history.replaceState,f=d(v,"push"),y=d(h,"replace"),a=o(()=>l("pop"),"popped");e.history.pushState=f,e.history.replaceState=y;const s=e.navBus?e.navBus.subscribe(a):void 0;s||e.addEventListener("popstate",a,{capture:!0});const m=o(()=>{p=!1,s?s():e.removeEventListener("popstate",a,{capture:!0}),e.history.pushState===f&&(e.history.pushState=v),e.history.replaceState===y&&(e.history.replaceState=h),c=void 0},"uninstall");return c=m,m}r(installRouteFlows,"installRouteFlows"),o(installRouteFlows,"installRouteFlows");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The router's pattern of the page on screen — what makes `/orders/42` and `/orders/43` one page (decision 9:
|
|
3
|
+
* identity is the router pattern only; react-router first).
|
|
4
|
+
*
|
|
5
|
+
* No app code: react-router renders every matched route through `RenderedRoute`, whose props are
|
|
6
|
+
* `{ match, routeContext: { outlet, matches, isDataRoute } }` (react-router 6.4 – 8, `lib/hooks.js`), and the deepest
|
|
7
|
+
* one's `matches` is the whole chain — a descendant `<Routes>` carries its parents' too. The fiber walk hands every
|
|
8
|
+
* component it visits to {@link routeMatchesOf}; the props are recognised by their SHAPE, not the component's name,
|
|
9
|
+
* so a minified build reads the same.
|
|
10
|
+
*/
|
|
11
|
+
interface RouteMatchLike {
|
|
12
|
+
route?: {
|
|
13
|
+
path?: unknown;
|
|
14
|
+
index?: unknown;
|
|
15
|
+
} | null;
|
|
16
|
+
}
|
|
17
|
+
/** The match chain when `props` are a `RenderedRoute`'s, else undefined. Cheap: a few property reads. */
|
|
18
|
+
export declare function routeMatchesOf(props: unknown): readonly RouteMatchLike[] | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The pattern a match chain spells: each route's `path` joined — an absolute child path starts over, an index or a
|
|
21
|
+
* layout route (no path) adds nothing, a splat stays `*`. `/companies/:id/onboarding/chat/canvas/:artifactId`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function patternOf(matches: readonly RouteMatchLike[]): string | undefined;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var f=Object.defineProperty;var n=(e,t)=>f(e,"name",{value:t,configurable:!0});var i=Object.defineProperty,c=n((e,t)=>i(e,"name",{value:t,configurable:!0}),"r");export function routeMatchesOf(e){if(!e||typeof e!="object")return;const t=e;if(!t.match||typeof t.match!="object"||!t.match.route||typeof t.match.route!="object")return;const r=t.routeContext?.matches;return Array.isArray(r)?r:void 0}n(routeMatchesOf,"routeMatchesOf"),c(routeMatchesOf,"routeMatchesOf");export function patternOf(e){if(!e.length)return;let t="";for(const a of e){const o=a?.route?.path;typeof o!="string"||o===""||(t=o.startsWith("/")?o:`${t.replace(/\/?\*$/,"")}/${o}`)}const r=`/${t.split("/").filter(Boolean).join("/")}`;return r.length>300?r.slice(0,300):r}n(patternOf,"patternOf"),c(patternOf,"patternOf");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/react-instrumentation",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@lensmcp/protocol-types": "1.25.
|
|
22
|
+
"@lensmcp/protocol-types": "1.25.7",
|
|
23
23
|
"tslib": "^2.3.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|