@lensmcp/nest-instrumentation 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/README.md CHANGED
@@ -82,7 +82,9 @@ Note that `forRoot` does **not** default `autoInstrumentMethods` on — only `cr
82
82
  - **Request flows** — an `APP_INTERCEPTOR` (`TraceInterceptor`) brackets every HTTP request. It reads `x-request-id`, `x-lensmcp-flow-id`, `x-lensmcp-origin-node-id`, and W3C `traceparent` headers, opens a `LensmcpRequestContext` in `AsyncLocalStorage`, and on completion emits a `server-request` event (method, route, status, duration) plus a span. Errors are resolved to their real HTTP status (via `HttpException.getStatus()` or a `status` / `statusCode` field) rather than Nest's not-yet-written default.
83
83
  - **Controller hops** — the interceptor times the controller method itself and emits a `Controller.handler (12ms)` span with `role: 'controller'`, so the trace shows the controller between the request and its services instead of skipping straight to the service.
84
84
  - **Provider-method traces** — when `trace.autoInstrumentMethods` is on, `LensmcpProviderTracker` walks the Nest container at bootstrap and wraps each provider's own prototype methods with a traced wrapper (in place, preserving `this`). Singleton instances are wrapped per-instance; request-scoped / transient providers are wrapped once on the shared prototype so every per-request instance is covered. Lifecycle hooks (`onModuleInit`, `onApplicationBootstrap`, etc.), getters/setters, methods already decorated with `@TraceMethod`, and anything marked `@LensmcpIgnore` are skipped.
85
- - **N+1 / fan-out detection** — each traced method snapshots the request's DB / Redis / external counters on entry; if a method issues ≥3 operations of the same class, it emits a `loop` warning (e.g. `loop in OrdersService.list (12 DB calls)`). DB call counts come from `@TraceMethod({ db: true })` and from the zero-touch taps in `@lensmcp/node-instrumentation` (pg, ioredis, fetch/http).
85
+ - **N+1 / fan-out detection** — each traced method snapshots the request's DB / Redis / external counters on entry; if it issues ≥3 operations of the same class that no method beneath it has already reported, it emits a `loop` warning (e.g. `loop in OrdersService.list (12 DB calls)`). Only the innermost method that owns the calls reports — its callers merely contain it. DB call counts come from `@TraceMethod({ db: true })` and from the zero-touch taps in `@lensmcp/node-instrumentation` (pg, ioredis, fetch/http).
86
+ - **What became of a call** — every span carries `outcome` (`returned` / `threw`) and, when it threw, `error` (`name`, a redacted and capped `message`, `code`).
87
+ - **Capture for a named method** — off by default. `LENSMCP_CAPTURE="Class.method[:args|:result|:both]"` (comma- or space-separated) or `@TraceMethod({ capture: 'both' })` puts that method's arguments and/or return value on its span: cut to a bounded shape before serialising, redacted, capped at 2000 characters.
86
88
  - **Provider graph & lifecycle** — every controller and provider emits a `singleton-instance` event at bootstrap (with module, scope, and a `generation` counter that increments on each `forRoot` / hot reload) and a `disposed` event on module destroy, so the agent can materialise the live provider tree and tell a new generation from a stale one.
87
89
  - **Memory** — when `memory.mode !== 'off'`, container fields (`Map` / `Set` / `Array`) on providers are tracked for size and growth via `@lensmcp/memory-tracker`. `scope: 'all'` watches every provider; `scope: 'tagged'` watches only those marked `@TraceProvider({ memory: true })`. Each request is bracketed as a "flow" so the suspect-leak detector can compare container sizes at request start vs. after it settles. `mode: 'deep'` additionally enables heap snapshots.
88
90
 
@@ -0,0 +1,12 @@
1
+ export type CaptureWhat = 'args' | 'result' | 'both';
2
+ export declare const CAPTURE_ENV = "LENSMCP_CAPTURE";
3
+ /** The most characters one captured value — the arguments, or the result — may take on an event. */
4
+ export declare const MAX_CAPTURE_CHARS = 2000;
5
+ /** `Class.method[:args|:result|:both]`, comma- or space-separated. A name with an unknown suffix is ignored whole. */
6
+ export declare function parseCaptureList(value: string | undefined): Map<string, CaptureWhat>;
7
+ /** What to capture of the method traced as `spanName`, if it was named in `LENSMCP_CAPTURE`. */
8
+ export declare function captureFor(spanName: string, env?: NodeJS.ProcessEnv): CaptureWhat | undefined;
9
+ /** One captured value as it goes on an event: bounded, redacted, JSON, capped. Never throws. */
10
+ export declare function capturedValue(value: unknown): string;
11
+ /** Test seam: forget the parsed list. */
12
+ export declare function resetCaptureForTests(): void;
package/lib/capture.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var b=Object.defineProperty;var a=(e,n)=>b(e,"name",{value:n,configurable:!0});var h=Object.defineProperty,c=a((e,n)=>h(e,"name",{value:n,configurable:!0}),"f");import{clip as A,redactDeep as C}from"@lensmcp/core/redact";export const CAPTURE_ENV="LENSMCP_CAPTURE",MAX_CAPTURE_CHARS=2e3;const g=4,d=40,l=20,y=500;export function parseCaptureList(e){const n=new Map;for(const r of(e??"").split(/[\s,]+/)){if(!r)continue;const t=r.lastIndexOf(":"),f=t>0?r.slice(0,t):r,o=t>0?r.slice(t+1):"args";f&&(o==="args"||o==="result"||o==="both")&&n.set(f,o)}return n}a(parseCaptureList,"parseCaptureList"),c(parseCaptureList,"parseCaptureList");let m,p=new Map;export function captureFor(e,n=process.env){const r=n[CAPTURE_ENV]??"";return r!==m&&(m=r,p=parseCaptureList(r)),p.size===0?void 0:p.get(e)}a(captureFor,"captureFor"),c(captureFor,"captureFor");function u(e,n,r){if(e==null)return e;if(typeof e=="string")return e.length>y?`${e.slice(0,y)}\u2026[${e.length-y} more chars]`:e;if(typeof e=="number"||typeof e=="boolean")return e;if(typeof e=="bigint")return`${e}n`;if(typeof e=="symbol")return e.toString();if(typeof e=="function")return`[Function ${e.name||"anonymous"}]`;if(e instanceof Date)return Number.isNaN(e.getTime())?"[Invalid Date]":e.toISOString();if(e instanceof Error)return{name:e.name,message:e.message};if(typeof Buffer<"u"&&Buffer.isBuffer(e))return`[Buffer ${e.length} bytes]`;if(r.has(e))return"[Circular]";if(n>=g)return Array.isArray(e)?`[Array(${e.length})]`:`[${e.constructor?.name??"Object"}]`;r.add(e);try{if(Array.isArray(e)){const i=e.slice(0,l).map(s=>u(s,n+1,r));return e.length>l&&i.push(`\u2026[${e.length-l} more items]`),i}if(e instanceof Map)return u(Object.fromEntries([...e].slice(0,d).map(([i,s])=>[String(i),s])),n,r);if(e instanceof Set)return u([...e],n,r);const t={};let f=0,o=0;for(const i of Object.keys(e)){if(f>=d){o+=1;continue}let s;try{s=e[i]}catch{s="[threw on read]"}t[i]=u(s,n+1,r),f+=1}return o>0&&(t["\u2026"]=`${o} more keys`),t}finally{r.delete(e)}}a(u,"p"),c(u,"prune");export function capturedValue(e){try{const n=JSON.stringify(C(u(e,0,new WeakSet),0,g+2));return A(n??"undefined",MAX_CAPTURE_CHARS)}catch{return"[could not be serialised]"}}a(capturedValue,"capturedValue"),c(capturedValue,"capturedValue");export function resetCaptureForTests(){m=void 0,p=new Map}a(resetCaptureForTests,"resetCaptureForTests"),c(resetCaptureForTests,"resetCaptureForTests");
package/lib/context.d.ts CHANGED
@@ -42,6 +42,14 @@ export interface LensmcpRequestContext {
42
42
  dbCallCount?: number;
43
43
  redisCallCount?: number;
44
44
  externalCallCount?: number;
45
+ /**
46
+ * Of those, the operations a traced method has already REPORTED as its loop. A frame that ends counts only what
47
+ * is not in here: a loop is the finding of the innermost frame that owns the calls, and every frame above it
48
+ * merely contains it (issues/closed/foodguard-56 — 44 `loop` events for a handful of loops, one per ancestor).
49
+ */
50
+ loopDbCalls?: number;
51
+ loopRedisCalls?: number;
52
+ loopExternalCalls?: number;
45
53
  /** Outbound east-west calls made so far by this request; numbers the child ids. */
46
54
  hopCount?: number;
47
55
  }
@@ -1,9 +1,12 @@
1
+ import { type CaptureWhat } from './capture.js';
1
2
  /** Marks a function already wrapped by traceWrap (avoids double-wrap). */
2
3
  export declare const LENSMCP_TRACED: unique symbol;
3
4
  /** Marks a class/method opted out of auto-instrumentation. */
4
5
  export declare const LENSMCP_IGNORE: unique symbol;
5
6
  export interface TraceWrapOptions {
6
7
  db?: boolean;
8
+ /** Capture this method's arguments and/or what it returned on its span (capture.ts). Off unless set here or named in `LENSMCP_CAPTURE`. */
9
+ capture?: CaptureWhat;
7
10
  }
8
11
  /** `@LensmcpIgnore()` — skip a class (all methods) or a single method. */
9
12
  export declare function LensmcpIgnore(): ClassDecorator & MethodDecorator;
package/lib/instrument.js CHANGED
@@ -1 +1 @@
1
- "use strict";var L=Object.defineProperty;var d=(e,o)=>L(e,"name",{value:o,configurable:!0});var v=Object.defineProperty,i=d((e,o)=>v(e,"name",{value:o,configurable:!0}),"i");import{fingerprint as m,ulid as y}from"@lensmcp/core";import{currentLensmcpContext as $}from"./context.js";import{activeOptions as E}from"./state.js";export const LENSMCP_TRACED=Symbol.for("@lensmcp/nest-instrumentation/traced"),LENSMCP_IGNORE=Symbol.for("@lensmcp/nest-instrumentation/ignore");export function LensmcpIgnore(){return((e,o)=>{if(o===void 0)e[LENSMCP_IGNORE]=!0;else{const l=e[o];typeof l=="function"&&(l[LENSMCP_IGNORE]=!0)}})}d(LensmcpIgnore,"LensmcpIgnore"),i(LensmcpIgnore,"LensmcpIgnore");export function isTraced(e){return typeof e=="function"&&!!e[LENSMCP_TRACED]}d(isTraced,"isTraced"),i(isTraced,"isTraced");export function isIgnored(e){return!!e&&(typeof e=="function"||typeof e=="object")&&!!e[LENSMCP_IGNORE]}d(isIgnored,"isIgnored"),i(isIgnored,"isIgnored");export function traceWrap(e,o,l={}){if(isTraced(e))return e;const p=l.db??!1,w=i(function(...f){let r;try{r=E()}catch{return e.apply(this,f)}if(!r.trace.services)return e.apply(this,f);const u=Date.now(),n=$(),q=n?.dbCallCount??0,k=n?.redisCallCount??0,D=n?.externalCallCount??0,C=i((t,s)=>{const a=Date.now()-u,I={id:y(),sessionId:r.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:t==="error"?"error":"info",context:{sessionId:r.sessionId,flowId:n?.flowId,requestId:n?.requestId},fingerprint:m({kind:"method-call",identity:o}),title:`${o} (${a}ms)${t==="error"?" \u2717":""}`,message:s instanceof Error?s.message:void 0,raw:{kind:"span",span:{name:o,flowId:n?.flowId,requestId:n?.requestId,startTime:u,endTime:Date.now(),durationMs:a,status:t}}};r.emit(I)},"emitSpan"),b=i(()=>{n&&(n.dbCallCount=(n.dbCallCount??0)+1),r.emit({id:y(),sessionId:r.sessionId,timestamp:Date.now(),source:"db",category:"db",severity:"info",context:{sessionId:r.sessionId,flowId:n?.flowId,requestId:n?.requestId},fingerprint:m({kind:"db-query",identity:o}),title:`query ${o}`,raw:{kind:"db-query",query:{signature:o}}})},"emitDbQuery"),g=i(()=>{if(p||!n)return;const t=(n.dbCallCount??0)-q,s=(n.redisCallCount??0)-k,a=(n.externalCallCount??0)-D;if(t<3&&s<3&&a<3)return;const I=Date.now()-u,c=[];t>=3&&c.push(`${t} DB calls`),s>=3&&c.push(`${s} redis calls`),a>=3&&c.push(`${a} external calls`),r.emit({id:y(),sessionId:r.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:"warning",context:{sessionId:r.sessionId,flowId:n.flowId,requestId:n.requestId},fingerprint:m({kind:"loop",identity:o}),title:`loop in ${o} (${c.join(", ")})`,raw:{kind:"loop",loop:{iterations:Math.max(t,s,a),durationMs:I,startedAt:u,dbCallsInsideLoop:t,redisCallsInsideLoop:s,externalCallsInsideLoop:a,awaitedOperationsInsideLoop:t+s+a}}})},"emitLoopIfNeeded"),x=i(()=>{p&&b(),C("ok"),g()},"onOk"),h=i(t=>{p&&b(),C("error",t),g()},"onErr");try{const t=e.apply(this,f);return t instanceof Promise?t.then(s=>(x(),s),s=>{throw h(s),s}):(x(),t)}catch(t){throw h(t),t}},"wrapped");return w[LENSMCP_TRACED]=!0,w}d(traceWrap,"traceWrap"),i(traceWrap,"traceWrap");
1
+ "use strict";var B=Object.defineProperty;var p=(e,r)=>B(e,"name",{value:r,configurable:!0});var _=Object.defineProperty,l=p((e,r)=>_(e,"name",{value:r,configurable:!0}),"i");import{fingerprint as g,ulid as b}from"@lensmcp/core";import{redactText as L}from"@lensmcp/core/redact";import{captureFor as F,capturedValue as M}from"./capture.js";import{currentLensmcpContext as G}from"./context.js";import{activeOptions as W}from"./state.js";export const LENSMCP_TRACED=Symbol.for("@lensmcp/nest-instrumentation/traced"),LENSMCP_IGNORE=Symbol.for("@lensmcp/nest-instrumentation/ignore");const w=300;function j(e){if(e instanceof Error){const d=e.code,u=L(e.message??"");return{name:e.name||e.constructor?.name||"Error",...u?{message:u.length>w?`${u.slice(0,w)}\u2026`:u}:{},...typeof d=="string"&&d.length<=64?{code:d}:{}}}const r=typeof e=="string"?L(e):void 0;return{name:e==null?"nothing":typeof e=="object"?"non-Error object":`non-Error ${typeof e}`,...r?{message:r.slice(0,w)}:{}}}p(j,"G"),l(j,"thrownOf");export function LensmcpIgnore(){return((e,r)=>{if(r===void 0)e[LENSMCP_IGNORE]=!0;else{const d=e[r];typeof d=="function"&&(d[LENSMCP_IGNORE]=!0)}})}p(LensmcpIgnore,"LensmcpIgnore"),l(LensmcpIgnore,"LensmcpIgnore");export function isTraced(e){return typeof e=="function"&&!!e[LENSMCP_TRACED]}p(isTraced,"isTraced"),l(isTraced,"isTraced");export function isIgnored(e){return!!e&&(typeof e=="function"||typeof e=="object")&&!!e[LENSMCP_IGNORE]}p(isIgnored,"isIgnored"),l(isIgnored,"isIgnored");export function traceWrap(e,r,d={}){if(isTraced(e))return e;const u=d.db??!1,h=l(function(...m){let i;try{i=W()}catch{return e.apply(this,m)}if(!i.trace.services)return e.apply(this,m);const f=Date.now(),o=G(),O=o?.dbCallCount??0,R=o?.redisCallCount??0,T=o?.externalCallCount??0,S=o?.loopDbCalls??0,P=o?.loopRedisCalls??0,N=o?.loopExternalCalls??0,I=d.capture??F(r),x=I==="args"||I==="both"?M(m):void 0,v=l((t,n,C)=>{const s=Date.now()-f,a={id:b(),sessionId:i.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:t==="error"?"error":"info",context:{sessionId:i.sessionId,flowId:o?.flowId,requestId:o?.requestId},fingerprint:g({kind:"method-call",identity:r}),title:`${r} (${s}ms)${t==="error"?" \u2717":""}`,message:n instanceof Error?n.message:void 0,raw:{kind:"span",span:{name:r,flowId:o?.flowId,requestId:o?.requestId,startTime:f,endTime:Date.now(),durationMs:s,status:t,outcome:t==="error"?"threw":"returned",...t==="error"?{error:j(n)}:{},...x!==void 0?{args:x}:{},...t==="ok"&&(I==="result"||I==="both")?{result:M(C)}:{}}}};i.emit(a)},"emitSpan"),D=l(()=>{o&&(o.dbCallCount=(o.dbCallCount??0)+1),i.emit({id:b(),sessionId:i.sessionId,timestamp:Date.now(),source:"db",category:"db",severity:"info",context:{sessionId:i.sessionId,flowId:o?.flowId,requestId:o?.requestId},fingerprint:g({kind:"db-query",identity:r}),title:`query ${r}`,raw:{kind:"db-query",query:{signature:r}}})},"emitDbQuery"),E=l(()=>{if(u||!o)return;const t=(o.dbCallCount??0)-O,n=(o.redisCallCount??0)-R,C=(o.externalCallCount??0)-T,s=Math.max(0,t-((o.loopDbCalls??0)-S)),a=Math.max(0,n-((o.loopRedisCalls??0)-P)),c=Math.max(0,C-((o.loopExternalCalls??0)-N));if(s<3&&a<3&&c<3)return;const A=Date.now()-f,y=[];s>=3&&y.push(`${s} DB calls`),a>=3&&y.push(`${a} redis calls`),c>=3&&y.push(`${c} external calls`),s>=3&&(o.loopDbCalls=(o.loopDbCalls??0)+s),a>=3&&(o.loopRedisCalls=(o.loopRedisCalls??0)+a),c>=3&&(o.loopExternalCalls=(o.loopExternalCalls??0)+c);const $=t-s+(n-a)+(C-c);i.emit({id:b(),sessionId:i.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:"warning",context:{sessionId:i.sessionId,flowId:o.flowId,requestId:o.requestId},fingerprint:g({kind:"loop",identity:r}),title:`loop in ${r} (${y.join(", ")})`,raw:{kind:"loop",loop:{iterations:Math.max(s,a,c),durationMs:A,startedAt:f,dbCallsInsideLoop:s,redisCallsInsideLoop:a,externalCallsInsideLoop:c,awaitedOperationsInsideLoop:s+a+c,...$>0?{reportedBeneath:$}:{}}}})},"emitLoopIfNeeded"),q=l(t=>{u&&D(),v("ok",void 0,t),E()},"onOk"),k=l(t=>{u&&D(),v("error",t),E()},"onErr");try{const t=e.apply(this,m);return t instanceof Promise?t.then(n=>(q(n),n),n=>{throw k(n),n}):(q(t),t)}catch(t){throw k(t),t}},"wrapped");return h[LENSMCP_TRACED]=!0,h}p(traceWrap,"traceWrap"),l(traceWrap,"traceWrap");
@@ -1,3 +1,3 @@
1
- "use strict";var U=Object.defineProperty;var u=(e,t)=>U(e,"name",{value:t,configurable:!0});var L=Object.defineProperty,n=u((e,t)=>L(e,"name",{value:t,configurable:!0}),"r");import{__decorate as X}from"tslib";import{Injectable as z}from"@nestjs/common";import{Observable as M}from"rxjs";import{catchError as B,tap as J}from"rxjs/operators";import{fingerprint as A,ulid as g}from"@lensmcp/core";import{activeMemoryOptions as K,onFlowStart as Q,onFlowSettled as V}from"@lensmcp/memory-tracker";import{LENSMCP_CONTEXT_STORAGE as W}from"./context.js";import{attachContextEnd as Y}from"./context-end.js";import{activeOptions as y}from"./state.js";let k=class{static{u(this,"h")}static{n(this,"TraceInterceptor")}intercept(e,t){const r=y();if(!r.trace.requests)return t.handle();const d=e.switchToHttp(),o=d.getRequest(),s=d.getResponse(),i=I(o.headers?.["x-request-id"])??g(),a={sessionId:r.sessionId,requestId:i,parentRequestId:I(o.headers?.["x-lensmcp-parent-request-id"]),flowId:I(o.headers?.["x-lensmcp-flow-id"]),originNodeId:I(o.headers?.["x-lensmcp-origin-node-id"]),traceparent:I(o.headers?.traceparent),dbCallCount:0},c=(o.method??"GET").toUpperCase(),m=o.route?.path??o.url??"<unknown>",p=e.getClass?.()?.name,w=e.getHandler?.()?.name,q=Date.now(),R=r.memory.mode!=="off";R&&Q(i);const $=n(()=>{if(!R)return;const f=K()?.settleMs??1e3;setTimeout(()=>V(i,a.flowId),f).unref?.()},"settleAndCheck"),F=Y(a);let T=!1,v=typeof s?.once!="function";const j=n(()=>{T&&v&&F()},"endIfOver");if(!v){const f=n(()=>{v=!0,j()},"responseOut");s.once?.("finish",f),s.once?.("close",f)}return new M(f=>{let S;return W.run(a,()=>{S=t.handle().pipe(J({next:n(l=>f.next(l),"next"),complete:n(()=>{C(r.projectName,a,c,m,s.statusCode??200,q),x(a,p,w,q,"ok"),$(),f.complete()},"complete")}),B(l=>{const O=E(l);if(O!==void 0||typeof s.once!="function")C(r.projectName,a,c,m,O??b(s),q,l);else{let D=!1;const N=n(G=>{if(D)return;D=!0;const H=G&&s.statusCode&&s.statusCode>=400?s.statusCode:b(s);C(r.projectName,a,c,m,H,q,l)},"settle");s.once("finish",()=>N(!0)),s.once("close",()=>N(!1))}return x(a,p,w,q,"error"),$(),f.error(l),new M})).subscribe()}),()=>{S?.unsubscribe(),T=!0,j()}})}};k=X([z()],k);export{k as TraceInterceptor};function E(e){const t=e;if(typeof t?.getStatus=="function")return t.getStatus();if(typeof t?.status=="number"&&t.status>=100)return t.status;if(typeof t?.statusCode=="number"&&t.statusCode>=100)return t.statusCode}u(E,"N"),n(E,"ownStatus");const Z=new Set(["TypeError","ReferenceError","SyntaxError","RangeError","EvalError","URIError"]);export function requestOutcome(e,t){return t instanceof Error&&Z.has(t.name)&&E(t)===void 0?"fault":e<400?"ok":e<500?"refused":e===500?"fault":"declared"}u(requestOutcome,"requestOutcome"),n(requestOutcome,"requestOutcome");function _(e){return e==="ok"?"info":e==="refused"?"warning":"error"}u(_,"Y"),n(_,"severityOf");function b(e){return e.statusCode&&e.statusCode>=400?e.statusCode:500}u(b,"_"),n(b,"inferStatus");function I(e){if(typeof e=="string")return e;if(Array.isArray(e)&&typeof e[0]=="string")return e[0]}u(I,"w"),n(I,"stringHeader");function x(e,t,r,d,o){if(!t||!r)return;const s=y(),i=Date.now()-d,a=`${t}.${r}`,c={id:g(),sessionId:e.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:o==="error"?"error":"info",context:{sessionId:e.sessionId,flowId:e.flowId,requestId:e.requestId},fingerprint:A({kind:"method-call",identity:a}),title:`${a} (${i}ms)${o==="error"?" \u2717":""}`,raw:{kind:"span",span:{name:a,role:"controller",flowId:e.flowId,requestId:e.requestId,startTime:d,endTime:Date.now(),durationMs:i,status:o}}};h(c,y().projectName),s.emit(c)}u(x,"A"),n(x,"emitControllerSpan");function P(e){if(!(e instanceof Error))return{message:String(e)};const t=e.stack?e.stack.split(`
1
+ "use strict";var B=Object.defineProperty;var d=(e,t)=>B(e,"name",{value:t,configurable:!0});var J=Object.defineProperty,n=d((e,t)=>J(e,"name",{value:t,configurable:!0}),"r");import{__decorate as K}from"tslib";import{Injectable as Q}from"@nestjs/common";import{Observable as _}from"rxjs";import{catchError as V,tap as W}from"rxjs/operators";import{fingerprint as C,headlessFlowId as Y,headlessFlowsEnabled as Z,isProbePath as ee,ulid as q}from"@lensmcp/core";import{activeMemoryOptions as te,onFlowStart as re,onFlowSettled as se}from"@lensmcp/memory-tracker";import{LENSMCP_CONTEXT_STORAGE as oe}from"./context.js";import{attachContextEnd as ne}from"./context-end.js";import{activeOptions as h}from"./state.js";let b=class{static{d(this,"C")}static{n(this,"TraceInterceptor")}intercept(e,t){const r=h();if(!r.trace.requests)return t.handle();const u=e.switchToHttp(),s=u.getRequest(),o=u.getResponse(),a=m(s.headers?.["x-request-id"])??q(),c=m(s.headers?.["x-lensmcp-flow-id"]),f=m(s.headers?.["x-internal-token"])!==void 0,I=c??(!f&&Z()&&!ee(s.url)?Y(a):void 0),i={sessionId:r.sessionId,requestId:a,parentRequestId:m(s.headers?.["x-lensmcp-parent-request-id"]),flowId:I,originNodeId:m(s.headers?.["x-lensmcp-origin-node-id"]),traceparent:m(s.headers?.traceparent),dbCallCount:0},l=(s.method??"GET").toUpperCase(),k=s.route?.path??s.url??"<unknown>",j=e.getClass?.()?.name,T=e.getHandler?.()?.name,w=Date.now();I&&!c&&G(i,l,k,w);const N=r.memory.mode!=="off";N&&re(a);const O=n(()=>{if(!N)return;const p=te()?.settleMs??1e3;setTimeout(()=>se(a,i.flowId),p).unref?.()},"settleAndCheck"),U=ne(i);let S=!1,$=typeof o?.once!="function";const D=n(()=>{S&&$&&U()},"endIfOver");if(!$){const p=n(()=>{$=!0,D()},"responseOut");o.once?.("finish",p),o.once?.("close",p)}return new _(p=>{let M;return oe.run(i,()=>{M=t.handle().pipe(W({next:n(g=>p.next(g),"next"),complete:n(()=>{v(r.projectName,i,l,k,o.statusCode??200,w),R(i,j,T,w,"ok"),O(),p.complete()},"complete")}),V(g=>{const A=x(g);if(A!==void 0||typeof o.once!="function")v(r.projectName,i,l,k,A??E(o),w,g);else{let P=!1;const F=n(X=>{if(P)return;P=!0;const z=X&&o.statusCode&&o.statusCode>=400?o.statusCode:E(o);v(r.projectName,i,l,k,z,w,g)},"settle");o.once("finish",()=>F(!0)),o.once("close",()=>F(!1))}return R(i,j,T,w,"error"),O(),p.error(g),new _})).subscribe()}),()=>{M?.unsubscribe(),S=!0,D()}})}};b=K([Q()],b);export{b as TraceInterceptor};function x(e){const t=e;if(typeof t?.getStatus=="function")return t.getStatus();if(typeof t?.status=="number"&&t.status>=100)return t.status;if(typeof t?.statusCode=="number"&&t.statusCode>=100)return t.statusCode}d(x,"A"),n(x,"ownStatus");const ae=new Set(["TypeError","ReferenceError","SyntaxError","RangeError","EvalError","URIError"]);export function requestOutcome(e,t){return t instanceof Error&&ae.has(t.name)&&x(t)===void 0?"fault":e<400?"ok":e<500?"refused":e===500?"fault":"declared"}d(requestOutcome,"requestOutcome"),n(requestOutcome,"requestOutcome");function H(e){return e==="ok"?"info":e==="refused"?"warning":"error"}d(H,"oe"),n(H,"severityOf");function E(e){return e.statusCode&&e.statusCode>=400?e.statusCode:500}d(E,"F"),n(E,"inferStatus");function m(e){if(typeof e=="string")return e;if(Array.isArray(e)&&typeof e[0]=="string")return e[0]}d(m,"I"),n(m,"stringHeader");function G(e,t,r,u){const s=h(),o=`request:${t} ${r}`,a={id:q(),sessionId:e.sessionId,timestamp:u,source:"nestjs",category:"backend",severity:"info",context:{sessionId:e.sessionId,flowId:e.flowId,requestId:e.requestId,originType:"request",originNodeId:o},fingerprint:C({kind:"flow-origin",identity:`${t}:${r}`}),title:`${t} ${r}`,raw:{kind:"flow-origin",originNodeId:o,method:t,route:r}};y(a,s.projectName),s.emit(a)}d(G,"se"),n(G,"emitRequestOrigin");function R(e,t,r,u,s){if(!t||!r)return;const o=h(),a=Date.now()-u,c=`${t}.${r}`,f={id:q(),sessionId:e.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:s==="error"?"error":"info",context:{sessionId:e.sessionId,flowId:e.flowId,requestId:e.requestId},fingerprint:C({kind:"method-call",identity:c}),title:`${c} (${a}ms)${s==="error"?" \u2717":""}`,raw:{kind:"span",span:{name:c,role:"controller",flowId:e.flowId,requestId:e.requestId,startTime:u,endTime:Date.now(),durationMs:a,status:s}}};y(f,h().projectName),o.emit(f)}d(R,"G"),n(R,"emitControllerSpan");function L(e){if(!(e instanceof Error))return{message:String(e)};const t=e.stack?e.stack.split(`
2
2
  `).slice(0,30).join(`
3
- `):void 0;return{...e.name?{name:e.name}:{},message:e.message,...t?{stack:t}:{}}}u(P,"Z"),n(P,"errorDetail");function h(e,t){if(!t)return;const r=e.raw;r&&r.project===void 0&&(r.project=t)}u(h,"C"),n(h,"stampProject");function C(e,t,r,d,o,s,i){const a=y(),c=Date.now()-s,m=requestOutcome(o,i),p={id:g(),sessionId:t.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:_(m),context:{sessionId:t.sessionId,flowId:t.flowId,requestId:t.requestId,...t.parentRequestId?{parentRequestId:t.parentRequestId}:{},originNodeId:t.originNodeId,traceparent:t.traceparent},fingerprint:A({kind:"server-request",identity:`${r}:${d}:${Math.floor(o/100)}xx`}),title:`${r} ${d} \u2192 ${o} (${c}ms)`,message:i instanceof Error?i.message:void 0,raw:{kind:"server-request",request:{method:r,route:d,status:o,outcome:m,durationMs:c,startedAt:s,endedAt:Date.now(),flowId:t.flowId,requestId:t.requestId,...t.parentRequestId?{parentRequestId:t.parentRequestId}:{},...i?{error:P(i)}:{}}}},w={...p,id:g(),raw:{kind:"span",span:{name:`nest.request ${r} ${d}`,traceId:t.traceparent,flowId:t.flowId,requestId:t.requestId,startTime:s,endTime:Date.now(),durationMs:c,status:m==="ok"?"ok":"error"}}};h(p,e),h(w,e),a.emit(p),a.emit(w)}u(C,"E"),n(C,"emitRequest");
3
+ `):void 0;return{...e.name?{name:e.name}:{},message:e.message,...t?{stack:t}:{}}}d(L,"re"),n(L,"errorDetail");function y(e,t){if(!t)return;const r=e.raw;r&&r.project===void 0&&(r.project=t)}d(y,"h"),n(y,"stampProject");function v(e,t,r,u,s,o,a){const c=h(),f=Date.now()-o,I=requestOutcome(s,a),i={id:q(),sessionId:t.sessionId,timestamp:Date.now(),source:"nestjs",category:"backend",severity:H(I),context:{sessionId:t.sessionId,flowId:t.flowId,requestId:t.requestId,...t.parentRequestId?{parentRequestId:t.parentRequestId}:{},originNodeId:t.originNodeId,traceparent:t.traceparent},fingerprint:C({kind:"server-request",identity:`${r}:${u}:${Math.floor(s/100)}xx`}),title:`${r} ${u} \u2192 ${s} (${f}ms)`,message:a instanceof Error?a.message:void 0,raw:{kind:"server-request",request:{method:r,route:u,status:s,outcome:I,durationMs:f,startedAt:o,endedAt:Date.now(),flowId:t.flowId,requestId:t.requestId,...t.parentRequestId?{parentRequestId:t.parentRequestId}:{},...a?{error:L(a)}:{}}}},l={...i,id:q(),raw:{kind:"span",span:{name:`nest.request ${r} ${u}`,traceId:t.traceparent,flowId:t.flowId,requestId:t.requestId,startTime:o,endTime:Date.now(),durationMs:f,status:I==="ok"?"ok":"error"}}};y(i,e),y(l,e),c.emit(i),c.emit(l)}d(v,"T"),n(v,"emitRequest");
@@ -7,6 +7,12 @@ export interface TraceMethodOptions {
7
7
  * enclosing method is flagged db-in-loop.
8
8
  */
9
9
  db?: boolean;
10
+ /**
11
+ * Capture what this method was handed (`'args'`), what it returned (`'result'`), or both, on its span — bounded,
12
+ * redacted and capped (capture.ts). For the one method whose argument IS the decision. Off by default; the same
13
+ * can be asked without touching source: `LENSMCP_CAPTURE="Class.method:both"`.
14
+ */
15
+ capture?: 'args' | 'result' | 'both';
10
16
  }
11
17
  /**
12
18
  * `@TraceMethod()` — decorate a service/controller method so every
@@ -1 +1 @@
1
- "use strict";var s=Object.defineProperty;var n=(e,t)=>s(e,"name",{value:t,configurable:!0});var f=Object.defineProperty,m=n((e,t)=>f(e,"name",{value:t,configurable:!0}),"u");import{traceWrap as p}from"./instrument.js";export function TraceMethod(e){const t=typeof e=="string"?{name:e}:e??{};return function(o,c,r){const a=r.value;if(typeof a!="function")return r;const u=o.constructor?.name??"Anonymous",i=t.name??`${u}.${String(c)}`;return r.value=p(a,i,{db:t.db}),r}}n(TraceMethod,"TraceMethod"),m(TraceMethod,"TraceMethod");
1
+ "use strict";var i=Object.defineProperty;var n=(r,e)=>i(r,"name",{value:e,configurable:!0});var s=Object.defineProperty,p=n((r,e)=>s(r,"name",{value:e,configurable:!0}),"u");import{traceWrap as m}from"./instrument.js";export function TraceMethod(r){const e=typeof r=="string"?{name:r}:r??{};return function(c,u,t){const a=t.value;if(typeof a!="function")return t;const o=c.constructor?.name??"Anonymous",f=e.name??`${o}.${String(u)}`;return t.value=m(a,f,{db:e.db,...e.capture?{capture:e.capture}:{}}),t}}n(TraceMethod,"TraceMethod"),p(TraceMethod,"TraceMethod");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensmcp/nest-instrumentation",
3
- "version": "1.24.2",
3
+ "version": "1.24.4",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -14,9 +14,9 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@lensmcp/core": "1.24.2",
18
- "@lensmcp/memory-tracker": "1.24.2",
19
- "@lensmcp/protocol-types": "1.24.2",
17
+ "@lensmcp/core": "1.24.4",
18
+ "@lensmcp/memory-tracker": "1.24.4",
19
+ "@lensmcp/protocol-types": "1.24.4",
20
20
  "tslib": "^2.3.0"
21
21
  },
22
22
  "peerDependencies": {