@nexussdk/tracker 0.0.3 → 0.0.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/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- 'use strict';var core=require('@nexussdk/core');var T=/^(password|passwd|token|secret|authorization|bearer|auth|credit_?card|cvv|cvc|ssn|api_?key|access_?token|refresh_?token)$/i,C=/\b(?:\d[ -]*?){13,16}\b/g,R=/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,y=/([?&](token|auth|key|secret|password|api_key|access_token|refresh_token)=)[^&]*/gi;function A(n){return n.replace(C,"[CARD_REDACTED]").replace(R,"[EMAIL_REDACTED]").replace(y,"$1[REDACTED]")}function s(n,e=0){if(e>5)return "[MaxDepthExceeded]";if(n==null)return n;if(typeof n=="string")return A(n);if(typeof n!="object")return n;if(Array.isArray(n))return n.map(r=>s(r,e+1));let t={};for(let[r,o]of Object.entries(n))T.test(r)?t[r]="[REDACTED]":t[r]=s(o,e+1);return t}function l(n){return n.replace(y,"$1[REDACTED]")}function f(n){if(!n)return [];let e=[],t=n.split(`
2
- `);for(let r of t){let o=r.trim(),i=o.match(/^at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)$/)||o.match(/^at\s+(.+?):(\d+):(\d+)$/)||o.match(/^at\s+\((.+?):(\d+):(\d+)\)$/);if(i){i.length===5?e.push({functionName:i[1]??"<anonymous>",fileName:i[2]??"<unknown>",lineNumber:parseInt(i[3]??"0",10),columnNumber:parseInt(i[4]??"0",10)}):i.length===4&&e.push({functionName:"<anonymous>",fileName:i[1]??"<unknown>",lineNumber:parseInt(i[2]??"0",10),columnNumber:parseInt(i[3]??"0",10)});continue}let a=o.match(/^(.+?)@(.+?):(\d+):(\d+)$/);a&&e.push({functionName:a[1]??"<anonymous>",fileName:a[2]??"<unknown>",lineNumber:parseInt(a[3]??"0",10),columnNumber:parseInt(a[4]??"0",10)});}return e}function g(n,e,t){let r=t?.fileName??"unknown",o=t?.lineNumber??0,i=`${n}:${e}:${r}:${o}`,a=5381;for(let d=0;d<i.length;d++)a=(a<<5)+a+i.charCodeAt(d)>>>0;return `fp_${a.toString(16).padStart(8,"0")}`}var c=class{buffer;listenersAttached=false;clickHandler;popStateHandler;constructor(e){this.buffer=new core.RingBuffer(e),this.clickHandler=this.handleClick.bind(this),this.popStateHandler=this.handleNavigation.bind(this);}push(e){let t=e.data?s(e.data):void 0;this.buffer.push({...e,data:t,timestamp:Date.now()});}getAll(){return this.buffer.toArray()}clear(){this.buffer.clear();}attachListeners(){this.listenersAttached||typeof window>"u"||(this.listenersAttached=true,document.addEventListener("click",this.clickHandler,{capture:true,passive:true}),window.addEventListener("popstate",this.popStateHandler,{passive:true}),this.interceptConsoleError());}detachListeners(){!this.listenersAttached||typeof window>"u"||(document.removeEventListener("click",this.clickHandler,{capture:true}),window.removeEventListener("popstate",this.popStateHandler),this.listenersAttached=false);}handleClick(e){let t=e.target;if(!t||t instanceof HTMLInputElement&&(t.type==="password"||t.hasAttribute("data-nexus-mask")))return;let r=this.describeElement(t);this.push({category:"ui.click",message:`Clicked ${r}`,level:"info",data:{elementTag:t.tagName.toLowerCase(),elementId:t.id||void 0,elementClass:t.className||void 0}});}handleNavigation(){this.push({category:"navigation",message:`Navigated to ${l(window.location.href)}`,level:"info",data:{url:l(window.location.href)}});}describeElement(e){let t=[e.tagName.toLowerCase()];return e.id&&t.push(`#${e.id}`),e.getAttribute("aria-label")&&t.push(`[aria-label="${e.getAttribute("aria-label")}"]`),t.join("")}interceptConsoleError(){let e=console.error.bind(console);console.error=(...t)=>{this.push({category:"console",message:t.map(String).join(" ").substring(0,500),level:"error"}),e(...t);};}};var u=class{options;maxRetries;isPageHiding=false;constructor(e){this.options=e,this.maxRetries=e.maxRetries??2,typeof document<"u"&&document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&(this.isPageHiding=true);}),typeof window<"u"&&window.addEventListener("pagehide",()=>{this.isPageHiding=true;});}send(e){let t=core.safeStringify(e);if(this.isPageHiding&&typeof navigator<"u"&&typeof navigator.sendBeacon=="function"){let r=new Blob([t],{type:"application/json"});navigator.sendBeacon(this.options.endpoint,r);return}this.sendWithRetry(t,0);}async flush(e){let t=core.safeStringify(e);if(typeof navigator<"u"&&typeof navigator.sendBeacon=="function"){let r=new Blob([t],{type:"application/json"});navigator.sendBeacon(this.options.endpoint,r);return}await this.sendWithRetry(t,0);}async sendWithRetry(e,t){try{let r=await fetch(this.options.endpoint,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.options.apiKey}`},body:e,keepalive:!0});if(r.ok)return;if(r.status>=500&&t<this.maxRetries){let o=core.computeBackoffMs(t,1e3,3e4);await new Promise(i=>setTimeout(i,o)),await this.sendWithRetry(e,t+1);}}catch{if(t<this.maxRetries){let r=core.computeBackoffMs(t,1e3,3e4);await new Promise(o=>setTimeout(o,r)),await this.sendWithRetry(e,t+1);}}}};function w(n){if(typeof window>"u")return ()=>{};let e=r=>{let o=r.error instanceof Error?r.error:new Error(r.message);n.captureError(o);},t=r=>{n.captureError(r.reason);};return window.addEventListener("error",e),window.addEventListener("unhandledrejection",t),()=>{window.removeEventListener("error",e),window.removeEventListener("unhandledrejection",t);}}var h=class{apiKey;environment;tags;beforeSend;breadcrumbManager;transport;userContext;dedupeMap=new Map;cleanupListeners;constructor(e={}){this.apiKey=core.resolveApiKey(e.apiKey);let t=core.resolveBaseUrl(e.baseUrl);this.environment=e.environment??"production",this.tags={...e.tags},this.beforeSend=e.beforeSend,this.breadcrumbManager=new c(Math.min(e.maxBreadcrumbs??20,50)),this.transport=new u({endpoint:`${t}/api/v1/telemetry/errors`,apiKey:this.apiKey}),e.autoCapture!==false&&typeof window<"u"&&(this.breadcrumbManager.attachListeners(),this.cleanupListeners=w(this));}captureError(e,t){let r=this.normalizeError(e),o=f(r.stack),i=g(r.type,r.message,o[0]),a=this.dedupeMap.get(i);if(a){a.count+=1;return}let d={fingerprint:i,errorType:r.type,errorMessage:r.message,stackTrace:o,breadcrumbs:this.breadcrumbManager.getAll(),userContext:this.userContext,deviceContext:this.getDeviceContext(),tags:{environment:this.environment,...this.tags,...t},occurrenceCount:1,clientTimestamp:Date.now()},v=s(d),m=this.beforeSend?this.beforeSend(v):v;if(!m)return;this.transport.send(m);let k=setTimeout(()=>{let p=this.dedupeMap.get(i);if(p&&p.count>1){let x={...p.lastPayload,occurrenceCount:p.count,clientTimestamp:Date.now()};this.transport.send(x);}this.dedupeMap.delete(i);},1e4);this.dedupeMap.set(i,{timer:k,count:1,lastPayload:m});}addBreadcrumb(e){this.breadcrumbManager.push(e);}setUser(e){this.userContext=e??void 0;}setTag(e,t){this.tags[e]=t;}async flush(){for(let[e,t]of this.dedupeMap.entries()){if(clearTimeout(t.timer),t.count>0){let r={...t.lastPayload,occurrenceCount:t.count,clientTimestamp:Date.now()};await this.transport.flush(r);}this.dedupeMap.delete(e);}}destroy(){this.cleanupListeners?.(),this.breadcrumbManager.detachListeners(),this.breadcrumbManager.clear(),this.dedupeMap.forEach(e=>clearTimeout(e.timer)),this.dedupeMap.clear();}normalizeError(e){return e instanceof Error?{type:e.name||"Error",message:e.message,stack:e.stack}:typeof e=="string"?{type:"UnhandledException",message:e}:{type:"NonErrorRejection",message:String(e)}}getDeviceContext(){let e=typeof window<"u"&&typeof navigator<"u";return {userAgent:e?navigator.userAgent:"Node/SSR",currentUrl:e?window.location.href:"",viewport:e?`${window.innerWidth}x${window.innerHeight}`:void 0,timezone:Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone,networkStatus:e&&"connection"in navigator?navigator.connection?.effectiveType??"unknown":void 0}}};exports.BreadcrumbManager=c;exports.NexusTrackerClient=h;exports.Transport=u;exports.computeFingerprint=g;exports.parseStackTrace=f;exports.sanitizeObject=s;exports.sanitizeUrl=l;
1
+ 'use strict';var core=require('@nexussdk/core');var _=/^(password|passwd|token|secret|authorization|bearer|auth|credit_?card|cvv|cvc|ssn|api_?key|access_?token|refresh_?token)$/i,I=/\b(?:\d[ -]*?){13,16}\b/g,L=/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,C=/([?&](token|auth|key|secret|password|api_key|access_token|refresh_token)=)[^&]*/gi;function R(i){return i.replace(I,"[CARD_REDACTED]").replace(L,"[EMAIL_REDACTED]").replace(C,"$1[REDACTED]")}function u(i,e=0){if(e>5)return "[MaxDepthExceeded]";if(i==null)return i;if(typeof i=="string")return R(i);if(typeof i!="object")return i;if(Array.isArray(i))return i.map(t=>u(t,e+1));let r={};for(let[t,n]of Object.entries(i))_.test(t)?r[t]="[REDACTED]":r[t]=u(n,e+1);return r}function b(i){return i.replace(C,"$1[REDACTED]")}function p(i){if(!i)return [];let e=[],r=i.split(`
2
+ `);for(let t of r){let n=t.trim(),o=n.match(/^at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)$/)||n.match(/^at\s+(.+?):(\d+):(\d+)$/)||n.match(/^at\s+\((.+?):(\d+):(\d+)\)$/);if(o){o.length===5?e.push({functionName:o[1]??"<anonymous>",fileName:o[2]??"<unknown>",lineNumber:parseInt(o[3]??"0",10),columnNumber:parseInt(o[4]??"0",10)}):o.length===4&&e.push({functionName:"<anonymous>",fileName:o[1]??"<unknown>",lineNumber:parseInt(o[2]??"0",10),columnNumber:parseInt(o[3]??"0",10)});continue}let s=n.match(/^(.+?)@(.+?):(\d+):(\d+)$/);s&&e.push({functionName:s[1]??"<anonymous>",fileName:s[2]??"<unknown>",lineNumber:parseInt(s[3]??"0",10),columnNumber:parseInt(s[4]??"0",10)});}return e}function f(i,e,r){let t=r?.fileName??"unknown",n=r?.lineNumber??0,o=`${i}:${e}:${t}:${n}`,s=5381;for(let c=0;c<o.length;c++)s=(s<<5)+s+o.charCodeAt(c)>>>0;return `fp_${s.toString(16).padStart(8,"0")}`}var m=class{buffer;listenersAttached=false;clickHandler;popStateHandler;constructor(e){this.buffer=new core.RingBuffer(e),this.clickHandler=this.handleClick.bind(this),this.popStateHandler=this.handleNavigation.bind(this);}push(e){let r=e.data?u(e.data):void 0;this.buffer.push({...e,data:r,timestamp:Date.now()});}getAll(){return this.buffer.toArray()}clear(){this.buffer.clear();}attachListeners(){this.listenersAttached||typeof window>"u"||(this.listenersAttached=true,document.addEventListener("click",this.clickHandler,{capture:true,passive:true}),window.addEventListener("popstate",this.popStateHandler,{passive:true}),this.interceptConsoleError());}detachListeners(){!this.listenersAttached||typeof window>"u"||(document.removeEventListener("click",this.clickHandler,{capture:true}),window.removeEventListener("popstate",this.popStateHandler),this.listenersAttached=false);}handleClick(e){let r=e.target;if(!r||r instanceof HTMLInputElement&&(r.type==="password"||r.hasAttribute("data-nexus-mask")))return;let t=this.describeElement(r);this.push({category:"ui.click",message:`Clicked ${t}`,level:"info",data:{elementTag:r.tagName.toLowerCase(),elementId:r.id||void 0,elementClass:r.className||void 0}});}handleNavigation(){this.push({category:"navigation",message:`Navigated to ${b(window.location.href)}`,level:"info",data:{url:b(window.location.href)}});}describeElement(e){let r=[e.tagName.toLowerCase()];return e.id&&r.push(`#${e.id}`),e.getAttribute("aria-label")&&r.push(`[aria-label="${e.getAttribute("aria-label")}"]`),r.join("")}interceptConsoleError(){let e=console.error.bind(console);console.error=(...r)=>{this.push({category:"console",message:r.map(String).join(" ").substring(0,500),level:"error"}),e(...r);};}};var k="nexus_events",x=200,g=class{options;maxRetries;plugin;isPageHiding=false;constructor(e){this.options=e,this.maxRetries=e.maxRetries??2,this.plugin=e.plugin??"fetch",this.plugin==="fetch"&&(typeof document<"u"&&document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&(this.isPageHiding=true);}),typeof window<"u"&&window.addEventListener("pagehide",()=>{this.isPageHiding=true;}));}send(e){if(typeof this.plugin=="function"){this.plugin(e);return}switch(this.plugin){case "console":this.sendConsole(e);return;case "localStorage":this.sendLocalStorage(e);return;default:this.sendFetch(e);}}async flush(e){if(typeof this.plugin=="function"){await this.plugin(e);return}switch(this.plugin){case "console":this.sendConsole(e);return;case "localStorage":this.sendLocalStorage(e);return;default:{let r=core.safeStringify(e);if(typeof navigator<"u"&&typeof navigator.sendBeacon=="function"){let t=new Blob([r],{type:"application/json"});navigator.sendBeacon(this.options.endpoint,t);return}await this.sendWithRetry(r,0);}}}sendFetch(e){let r=core.safeStringify(e);if(this.isPageHiding&&typeof navigator<"u"&&typeof navigator.sendBeacon=="function"){let t=new Blob([r],{type:"application/json"});navigator.sendBeacon(this.options.endpoint,t);return}this.sendWithRetry(r,0);}sendConsole(e){let r=`[Nexus|${e.errorType}]`;console.group(r,e.errorMessage),console.warn("Fingerprint:",e.fingerprint),console.warn("Stack frames:",e.stackTrace.slice(0,3)),e.breadcrumbs.length>0&&console.warn("Last breadcrumbs:",e.breadcrumbs.slice(-5)),e.userContext&&console.warn("User:",e.userContext),console.groupEnd();}sendLocalStorage(e){if(!(typeof localStorage>"u"))try{let r=localStorage.getItem(k),t=r?JSON.parse(r):[];t.push(e),t.length>x&&t.splice(0,t.length-x),localStorage.setItem(k,core.safeStringify(t));}catch{}}async sendWithRetry(e,r){try{let t=await fetch(this.options.endpoint,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.options.apiKey}`},body:e,keepalive:!0});if(t.ok)return;if(t.status>=500&&r<this.maxRetries){let n=core.computeBackoffMs(r,1e3,3e4);await new Promise(o=>setTimeout(o,n)),await this.sendWithRetry(e,r+1);}}catch{if(r<this.maxRetries){let t=core.computeBackoffMs(r,1e3,3e4);await new Promise(n=>setTimeout(n,t)),await this.sendWithRetry(e,r+1);}}}};function S(i){if(typeof window>"u")return ()=>{};let e=t=>{let n=t.error instanceof Error?t.error:new Error(t.message);i.captureError(n);},r=t=>{i.captureError(t.reason);};return window.addEventListener("error",e),window.addEventListener("unhandledrejection",r),()=>{window.removeEventListener("error",e),window.removeEventListener("unhandledrejection",r);}}var w=class{apiKey;environment;tags;extras;beforeSend;breadcrumbManager;transport;samplingRate;dedupeWindowMs;maxPerSession;sessionCaptureCount=0;userContext;dedupeMap=new Map;cleanupListeners;constructor(e={}){this.apiKey=core.resolveApiKey(e.apiKey);let r=core.resolveBaseUrl(e.baseUrl);this.environment=e.environment??"production",this.tags={...e.tags},this.extras={},this.beforeSend=e.beforeSend,this.samplingRate=Math.max(0,Math.min(1,e.sampling?.rate??1)),this.dedupeWindowMs=e.sampling?.dedupeWindow??1e4,this.maxPerSession=e.sampling?.maxPerSession??100,this.breadcrumbManager=new m(Math.min(e.maxBreadcrumbs??20,50)),this.transport=new g({endpoint:`${r}/api/v1/telemetry/errors`,apiKey:this.apiKey,plugin:e.transport}),e.autoCapture!==false&&typeof window<"u"&&(this.breadcrumbManager.attachListeners(),this.cleanupListeners=S(this));}captureError(e,r){if(this.sessionCaptureCount>=this.maxPerSession||this.samplingRate<1&&Math.random()>this.samplingRate)return;let t=this.normalizeError(e),n=p(t.stack),o=f(t.type,t.message,n[0]),s=this.dedupeMap.get(o);if(s){s.count+=1;return}let c={fingerprint:o,errorType:t.type,errorMessage:t.message,stackTrace:n,breadcrumbs:this.breadcrumbManager.getAll(),userContext:this.userContext,deviceContext:this.getDeviceContext(),tags:{environment:this.environment,...this.tags,...r},occurrenceCount:1,clientTimestamp:Date.now()},l=u(c),a=this.beforeSend?this.beforeSend(l):l;if(!a)return;this.transport.send(a),this.sessionCaptureCount++;let h=setTimeout(()=>{let y=this.dedupeMap.get(o);if(y&&y.count>1){let N={...y.lastPayload,occurrenceCount:y.count,clientTimestamp:Date.now()};this.transport.send(N);}this.dedupeMap.delete(o);},this.dedupeWindowMs);this.dedupeMap.set(o,{timer:h,count:1,lastPayload:a});}captureMessage(e,r="info",t){let n=new Error(e);n.name="NexusMessage",this.captureError(n,{_level:r,...t});}addBreadcrumb(e){this.breadcrumbManager.push(e);}setUser(e){this.userContext=e??void 0;}setTag(e,r){this.tags[e]=r;}setExtra(e,r){this.extras[e]=r;}async flush(){for(let[e,r]of this.dedupeMap.entries()){if(clearTimeout(r.timer),r.count>0){let t={...r.lastPayload,occurrenceCount:r.count,clientTimestamp:Date.now()};await this.transport.flush(t);}this.dedupeMap.delete(e);}}destroy(){this.cleanupListeners?.(),this.breadcrumbManager.detachListeners(),this.breadcrumbManager.clear(),this.dedupeMap.forEach(e=>clearTimeout(e.timer)),this.dedupeMap.clear();}normalizeError(e){return e instanceof Error?{type:e.name||"Error",message:e.message,stack:e.stack}:typeof e=="string"?{type:"UnhandledException",message:e}:{type:"NonErrorRejection",message:String(e)}}getDeviceContext(){let e=typeof window<"u"&&typeof navigator<"u";return {userAgent:e?navigator.userAgent:"Node/SSR",currentUrl:e?window.location.href:"",viewport:e?`${window.innerWidth}x${window.innerHeight}`:void 0,timezone:Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone,networkStatus:e&&"connection"in navigator?navigator.connection?.effectiveType??"unknown":void 0}}};var T=class{_error=null;_errorInfo=null;_listeners=new Set;_tracker;_onError;_tags;_generateErrorId;constructor(e={}){this._tracker=e.tracker,this._onError=e.onError,this._tags=e.tags,this._generateErrorId=e.generateErrorId??(r=>{let t=p(r.stack);return `NX-${f(r.name,r.message,t[0]).replace(/^fp_/,"").slice(0,8).toUpperCase()}`});}get hasError(){return this._error!==null}get currentError(){return this._error}get errorInfo(){return this._errorInfo}capture(e,r){let t=e instanceof Error?e:new Error(String(e));this._error=t;let n=this._generateErrorId(t),o={error:t,errorId:n,reset:()=>this.recover(),componentStack:r?.componentStack};if(this._errorInfo=o,this._tracker)try{this._tracker.captureError(t,{tags:{...this._tags,...r?.tags,guardErrorId:n},componentStack:r?.componentStack});}catch(s){typeof console<"u"&&console.warn("[NexusGuardCore] Failed to forward error to tracker:",s);}if(this._onError)try{this._onError(t,o);}catch(s){typeof console<"u"&&console.warn("[NexusGuardCore] onError callback threw:",s);}return this._notify(),o}recover(){this._error!==null&&(this._error=null,this._errorInfo=null,this._notify());}reset(){this.recover();}subscribe(e){return this._listeners.add(e),e(this._errorInfo),()=>{this._listeners.delete(e);}}_notify(){for(let e of this._listeners)try{e(this._errorInfo);}catch(r){typeof console<"u"&&console.error("[NexusGuardCore] Subscriber threw error:",r);}}};var F={LCP:[2500,4e3],CLS:[.1,.25],FID:[100,300],TTFB:[800,1800],INP:[200,500],FCP:[1800,3e3]};function d(i,e){let[r,t]=F[i]??[1/0,1/0];return e<=r?"good":e<=t?"needs-improvement":"poor"}function v(){return typeof performance>"u"?"navigate":performance.getEntriesByType("navigation")[0]?.type??"navigate"}function B(i){if(typeof PerformanceObserver>"u")return ()=>{};let e=0,r=null;try{r=new PerformanceObserver(o=>{let s=o.getEntries(),c=s[s.length-1];if(!c)return;e=c.startTime??0;}),r.observe({type:"largest-contentful-paint",buffered:!0});}catch{return ()=>{}}let t=()=>{e!==0&&i({name:"LCP",value:e,rating:d("LCP",e),delta:e,navigationType:v()});},n=()=>t();return document.addEventListener("visibilitychange",n,{once:true}),()=>{r?.disconnect(),document.removeEventListener("visibilitychange",n);}}function D(i){if(typeof PerformanceObserver>"u")return ()=>{};let e=0,r=null;try{r=new PerformanceObserver(o=>{for(let s of o.getEntries()){let c=s;c.hadRecentInput||(e+=c.value??0);}}),r.observe({type:"layout-shift",buffered:!0});}catch{return ()=>{}}let t=()=>{e!==0&&i({name:"CLS",value:e,rating:d("CLS",e),delta:e,navigationType:v()});},n=()=>t();return document.addEventListener("visibilitychange",n,{once:true}),()=>{r?.disconnect(),document.removeEventListener("visibilitychange",n);}}function $(i){if(typeof PerformanceObserver>"u")return ()=>{};let e=null;try{e=new PerformanceObserver(r=>{let n=r.getEntries()[0];if(!n)return;let o=n.processingStart-n.startTime;i({name:"FID",value:o,rating:d("FID",o),delta:o,navigationType:v()}),e?.disconnect();}),e.observe({type:"first-input",buffered:!0});}catch{return ()=>{}}return ()=>e?.disconnect()}function V(i){if(typeof PerformanceObserver>"u")return ()=>{};try{let r=performance.getEntriesByType("navigation")[0];if(r){let t=r.responseStart-r.requestStart;i({name:"TTFB",value:t,rating:d("TTFB",t),delta:t,navigationType:r.type??"navigate"});}}catch{}return ()=>{}}function H(i){if(typeof PerformanceObserver>"u")return ()=>{};let e=0,r=null;try{r=new PerformanceObserver(o=>{for(let s of o.getEntries()){let c=s;c.duration>e&&(e=c.duration);}}),r.observe({type:"event",buffered:!0,durationThreshold:16});}catch{return ()=>{}}let t=()=>{e!==0&&i({name:"INP",value:e,rating:d("INP",e),delta:e,navigationType:v()});},n=()=>t();return document.addEventListener("visibilitychange",n,{once:true}),()=>{r?.disconnect(),document.removeEventListener("visibilitychange",n);}}function j(i){if(typeof PerformanceObserver>"u")return ()=>{};let e=null;try{e=new PerformanceObserver(r=>{let n=r.getEntries().find(s=>s.name==="first-contentful-paint");if(!n)return;let o=n.startTime;i({name:"FCP",value:o,rating:d("FCP",o),delta:o,navigationType:v()}),e?.disconnect();}),e.observe({type:"paint",buffered:!0});}catch{return ()=>{}}return ()=>e?.disconnect()}function z(i,e){if(typeof PerformanceObserver>"u")return ()=>{};let r=null;try{r=new PerformanceObserver(t=>{for(let n of t.getEntries())n.duration>=e&&i.addBreadcrumb({category:"custom",message:`Long task detected: ${Math.round(n.duration)}ms`,level:n.duration>=e*3?"error":"warning",data:{durationMs:Math.round(n.duration),startTime:Math.round(n.startTime)}});}),r.observe({type:"longtask",buffered:!0});}catch{return ()=>{}}return ()=>r?.disconnect()}var U={LCP:B,CLS:D,FID:$,TTFB:V,INP:H,FCP:j};function W(i,e={}){if(typeof window>"u")return ()=>{};let{captureAsEvents:r=false,poorRatingOnly:t=false,vitals:n=["LCP","CLS","FID","TTFB","INP","FCP"],longTaskThresholdMs:o=50}=e,s=a=>{t&&a.rating==="good"||(i.addBreadcrumb({category:"custom",message:`Web Vital ${a.name}: ${a.rating.toUpperCase()} (${Math.round(a.value)}${a.name==="CLS"?"":"ms"})`,level:a.rating==="poor"?"error":a.rating==="needs-improvement"?"warning":"info",data:{metric:a.name,value:a.value,rating:a.rating,navigationType:a.navigationType}}),r&&a.rating!=="good"&&i.captureMessage(`Performance degradation: ${a.name} is ${a.rating} (${Math.round(a.value)}${a.name==="CLS"?"":"ms"})`,a.rating==="poor"?"error":"warning",{metric:a.name,value:a.value,rating:a.rating,navigationType:a.navigationType}));},c=n.map(a=>{let h=U[a];return h?h(s):()=>{}}),l=z(i,o);return ()=>{c.forEach(a=>a()),l();}}exports.BreadcrumbManager=m;exports.NexusGuardCore=T;exports.NexusTrackerClient=w;exports.Transport=g;exports.attachWebVitals=W;exports.computeFingerprint=f;exports.parseStackTrace=p;exports.sanitizeObject=u;exports.sanitizeUrl=b;
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { Breadcrumb, UserContext, ErrorEventPayload, StackFrame } from '@nexussdk/contracts';
1
+ import { SeverityLevel, Breadcrumb, UserContext, ErrorEventPayload, SamplingConfig, TransportPlugin, StackFrame, NexusErrorInfo, PerformanceVitalsOptions } from '@nexussdk/contracts';
2
2
 
3
3
  /**
4
4
  * @fileoverview NexusTrackerClient — Full-featured crash ingestion and telemetry SDK.
5
- * Automated global error capture, PII sanitization, deduplication, and hybrid transport.
5
+ * Automated global error capture, PII sanitization, deduplication, sampling, and hybrid transport.
6
6
  * @module @nexus/sdk-tracker/client
7
7
  */
8
8
 
@@ -15,6 +15,8 @@ import { Breadcrumb, UserContext, ErrorEventPayload, StackFrame } from '@nexussd
15
15
  * environment: 'production',
16
16
  * autoCapture: true,
17
17
  * maxBreadcrumbs: 20,
18
+ * sampling: { rate: 0.5, dedupeWindow: 5000 },
19
+ * transport: 'console', // development mode
18
20
  * });
19
21
  */
20
22
  interface NexusTrackerOptions {
@@ -29,10 +31,12 @@ interface NexusTrackerOptions {
29
31
  baseUrl?: string;
30
32
  /**
31
33
  * Target deployment environment ('production' | 'staging' | 'development').
34
+ * @default 'production'
32
35
  */
33
36
  environment?: string;
34
37
  /**
35
38
  * Max breadcrumbs retained in ring buffer. Defaults to 20 (max 50).
39
+ * @default 20
36
40
  */
37
41
  maxBreadcrumbs?: number;
38
42
  /**
@@ -41,13 +45,39 @@ interface NexusTrackerOptions {
41
45
  tags?: Record<string, string>;
42
46
  /**
43
47
  * Toggle automated capturing of uncaught exceptions. Defaults to true.
48
+ * @default true
44
49
  */
45
50
  autoCapture?: boolean;
46
51
  /**
47
52
  * Callback hook to inspect, mutate, or drop an event before dispatch.
48
53
  * Return null to drop the event completely.
54
+ *
55
+ * @example
56
+ * beforeSend: (event) => {
57
+ * if (event.tags?.environment === 'test') return null; // drop test events
58
+ * return event;
59
+ * }
49
60
  */
50
61
  beforeSend?: (event: ErrorEventPayload) => ErrorEventPayload | null;
62
+ /**
63
+ * Client-side event sampling and rate limiting configuration.
64
+ *
65
+ * @example
66
+ * sampling: { rate: 0.5, dedupeWindow: 5000, maxPerSession: 25 }
67
+ */
68
+ sampling?: SamplingConfig;
69
+ /**
70
+ * Transport plugin for event delivery.
71
+ * - `'fetch'` (default) — HTTP POST to ingestion endpoint
72
+ * - `'console'` — log to browser console (development)
73
+ * - `'localStorage'` — buffer events in localStorage
74
+ * - `Function` — custom async handler
75
+ *
76
+ * @example
77
+ * transport: 'console'
78
+ * transport: async (event) => await myLogger.log(event)
79
+ */
80
+ transport?: TransportPlugin;
51
81
  }
52
82
  /**
53
83
  * Public interface contract for NexusTrackerClient.
@@ -63,6 +93,19 @@ interface INexusTrackerClient {
63
93
  * tracker.captureError(new TypeError('Payment failed'), { checkoutStep: 'payment' });
64
94
  */
65
95
  captureError(error: unknown, extra?: Record<string, unknown>): void;
96
+ /**
97
+ * Captures a custom message event (non-exception telemetry).
98
+ * Useful for business-logic events, warnings, or diagnostic checkpoints.
99
+ *
100
+ * @param message - Human-readable message string.
101
+ * @param level - Severity level. Defaults to 'info'.
102
+ * @param extra - Optional metadata tags attached to the event.
103
+ *
104
+ * @example
105
+ * tracker.captureMessage('checkout_step_completed', 'info', { step: 3, cartTotal: 129.99 });
106
+ * tracker.captureMessage('payment_gateway_slow', 'warning', { latencyMs: 4500 });
107
+ */
108
+ captureMessage(message: string, level?: SeverityLevel, extra?: Record<string, unknown>): void;
66
109
  /**
67
110
  * Records a user activity step into the chronological breadcrumb ring buffer.
68
111
  *
@@ -83,14 +126,28 @@ interface INexusTrackerClient {
83
126
  setUser(user: UserContext | null): void;
84
127
  /**
85
128
  * Dynamically sets or updates a persistent search tag.
129
+ * Tags are attached to all subsequent events.
86
130
  *
87
131
  * @param key - Tag key name.
88
132
  * @param value - Tag value string.
89
133
  *
90
134
  * @example
91
135
  * tracker.setTag('app_version', '2.4.1');
136
+ * tracker.setTag('ab_test', 'variant_b');
92
137
  */
93
138
  setTag(key: string, value: string): void;
139
+ /**
140
+ * Sets an unstructured extra metadata value.
141
+ * Unlike tags (strings only), extras accept any serializable value.
142
+ *
143
+ * @param key - Extra key name.
144
+ * @param value - Any serializable value (object, number, array, etc.).
145
+ *
146
+ * @example
147
+ * tracker.setExtra('cart_items', 3);
148
+ * tracker.setExtra('feature_flags', { checkout_v2: true });
149
+ */
150
+ setExtra(key: string, value: unknown): void;
94
151
  /**
95
152
  * Flushes any buffered events immediately via navigator.sendBeacon or fetch.
96
153
  *
@@ -116,14 +173,20 @@ interface INexusTrackerClient {
116
173
  * @example
117
174
  * const tracker = new NexusTrackerClient({ apiKey: 'pk_live_...' });
118
175
  * tracker.captureError(new Error('Checkout failed'));
176
+ * tracker.captureMessage('cart_abandoned', 'warning', { items: 3 });
119
177
  */
120
178
  declare class NexusTrackerClient implements INexusTrackerClient {
121
179
  private readonly apiKey;
122
180
  private readonly environment;
123
181
  private readonly tags;
182
+ private readonly extras;
124
183
  private readonly beforeSend?;
125
184
  private readonly breadcrumbManager;
126
185
  private readonly transport;
186
+ private readonly samplingRate;
187
+ private readonly dedupeWindowMs;
188
+ private readonly maxPerSession;
189
+ private sessionCaptureCount;
127
190
  private userContext?;
128
191
  private readonly dedupeMap;
129
192
  private cleanupListeners?;
@@ -132,6 +195,11 @@ declare class NexusTrackerClient implements INexusTrackerClient {
132
195
  * Captures an error exception, generates fingerprint, scrubs PII, and enqueues transmission.
133
196
  */
134
197
  captureError(error: unknown, extra?: Record<string, unknown>): void;
198
+ /**
199
+ * Captures a custom diagnostic message as a synthetic error event.
200
+ * The errorType is set to 'NexusMessage' with the level encoded in tags.
201
+ */
202
+ captureMessage(message: string, level?: SeverityLevel, extra?: Record<string, unknown>): void;
135
203
  /**
136
204
  * Records a contextual breadcrumb in the ring buffer.
137
205
  */
@@ -144,6 +212,10 @@ declare class NexusTrackerClient implements INexusTrackerClient {
144
212
  * Sets a custom tag associated with error events.
145
213
  */
146
214
  setTag(key: string, value: string): void;
215
+ /**
216
+ * Sets an unstructured extra metadata value attached to all subsequent events.
217
+ */
218
+ setExtra(key: string, value: unknown): void;
147
219
  /**
148
220
  * Flushes all pending deduplication queues and dispatches queued payloads immediately.
149
221
  */
@@ -294,6 +366,7 @@ declare class BreadcrumbManager {
294
366
 
295
367
  /**
296
368
  * @fileoverview Hybrid transport dispatcher for error payload delivery.
369
+ * Supports pluggable transport: fetch (default), console, localStorage, or custom function.
297
370
  * Prefers fetch with keepalive; falls back to navigator.sendBeacon during page unload.
298
371
  * @module @nexus/sdk-tracker/transport
299
372
  */
@@ -302,54 +375,188 @@ declare class BreadcrumbManager {
302
375
  * Options for configuring the transport dispatcher.
303
376
  */
304
377
  interface TransportOptions {
305
- /** Go-Gin ingestion endpoint URL. */
378
+ /** Go-Gin ingestion endpoint URL. Used only when plugin is 'fetch' (default). */
306
379
  endpoint: string;
307
- /** Public API key for Authorization header. */
380
+ /** Public API key for Authorization header. Used only when plugin is 'fetch'. */
308
381
  apiKey: string;
309
- /** Maximum retry attempts on network failures. Defaults to 2. */
382
+ /** Maximum retry attempts on network failures. Defaults to 2. Applied to 'fetch' only. */
310
383
  maxRetries?: number;
384
+ /**
385
+ * Transport plugin selection.
386
+ * - `'fetch'` (default) — standard HTTP POST to ingestion endpoint with retry
387
+ * - `'console'` — logs event to console.warn (ideal for development)
388
+ * - `'localStorage'` — persists event JSON to localStorage under key `nexus_events`
389
+ * - `Function` — custom async dispatcher; receives the full ErrorEventPayload
390
+ *
391
+ * @example
392
+ * // Development: log to console only
393
+ * new Transport({ endpoint: '...', apiKey: '...', plugin: 'console' })
394
+ *
395
+ * @example
396
+ * // Custom forwarding transport
397
+ * new Transport({ endpoint: '...', apiKey: '...', plugin: async (event) => {
398
+ * await fetch('/my-logging-endpoint', { method: 'POST', body: JSON.stringify(event) });
399
+ * }})
400
+ */
401
+ plugin?: TransportPlugin;
311
402
  }
312
403
  /**
313
404
  * Hybrid transport dispatcher that intelligently selects the delivery mechanism:
314
405
  * - **Normal execution**: `fetch(url, { keepalive: true })` with retry
315
406
  * - **Page teardown** (`visibilityState === 'hidden'` or `pagehide`): `navigator.sendBeacon`
407
+ * - **Plugin override**: delegates entirely to the configured plugin strategy
316
408
  *
317
409
  * @example
410
+ * // Default fetch transport
318
411
  * const transport = new Transport({
319
412
  * endpoint: 'http://localhost:8080/api/v1/telemetry/errors',
320
413
  * apiKey: 'pk_live_...',
321
414
  * });
322
- * transport.send(errorPayload);
415
+ *
416
+ * @example
417
+ * // Console transport (development)
418
+ * const transport = new Transport({
419
+ * endpoint: 'http://localhost:8080/api/v1/telemetry/errors',
420
+ * apiKey: 'pk_dev_...',
421
+ * plugin: 'console',
422
+ * });
323
423
  */
324
424
  declare class Transport {
325
425
  private readonly options;
326
426
  private readonly maxRetries;
427
+ private readonly plugin;
327
428
  private isPageHiding;
328
429
  constructor(options: TransportOptions);
329
430
  /**
330
- * Dispatches an error payload to the Go-Gin ingestion endpoint.
331
- * Automatically selects fetch or sendBeacon based on page lifecycle state.
431
+ * Dispatches an error payload via the configured transport plugin.
432
+ * Automatically selects fetch or sendBeacon when plugin is 'fetch'.
332
433
  *
333
434
  * @param payload - The sanitized {@link ErrorEventPayload} to transmit.
334
- *
335
- * @example
336
- * transport.send({
337
- * fingerprint: 'fp_abc123',
338
- * errorType: 'TypeError',
339
- * errorMessage: "Cannot read property 'map' of undefined",
340
- * // ...
341
- * });
342
435
  */
343
436
  send(payload: ErrorEventPayload): void;
344
437
  /**
345
- * Forces immediate flush of all pending events using sendBeacon.
346
- * Called during manual flush or SDK destroy lifecycle.
438
+ * Forces immediate flush of all pending events.
439
+ * Delegates to the active plugin strategy.
347
440
  *
348
441
  * @param payload - The error payload to flush.
349
- * @returns Promise that resolves when the beacon is dispatched.
350
442
  */
351
443
  flush(payload: ErrorEventPayload): Promise<void>;
444
+ private sendFetch;
445
+ private sendConsole;
446
+ private sendLocalStorage;
352
447
  private sendWithRetry;
353
448
  }
354
449
 
355
- export { BreadcrumbManager, type INexusTrackerClient, NexusTrackerClient, type NexusTrackerOptions, Transport, type TransportOptions, computeFingerprint, parseStackTrace, sanitizeObject, sanitizeUrl };
450
+ /**
451
+ * @fileoverview Framework-agnostic Error Boundary core logic for Nexus SDK.
452
+ * Encapsulates error capture, fingerprint extraction, recovery state, and subscriber dispatch.
453
+ * Used directly by Vanilla integrations and underpins React / Vue / Angular adapters.
454
+ * @module @nexus/sdk-tracker/guard
455
+ */
456
+
457
+ interface NexusGuardCoreOptions {
458
+ /** Tracker client instance used to forward captured errors. Optional. */
459
+ tracker?: NexusTrackerClient;
460
+ /** Callback fired immediately upon capturing an unhandled boundary exception. */
461
+ onError?: (error: Error, info: NexusErrorInfo) => void;
462
+ /** Static tags attached to all errors captured by this guard instance. */
463
+ tags?: Record<string, string>;
464
+ /** Custom error ID generator function. Defaults to computeFingerprint hash. */
465
+ generateErrorId?: (error: Error) => string;
466
+ }
467
+ /**
468
+ * Framework-agnostic Error Boundary controller.
469
+ * Maintains boundary state, generates support reference fingerprints,
470
+ * notifies listeners, and optionally forwards errors to NexusTrackerClient.
471
+ *
472
+ * @example
473
+ * // Vanilla JS DOM boundary
474
+ * const guard = new NexusGuardCore({ tracker });
475
+ * guard.subscribe((info) => {
476
+ * if (info) {
477
+ * container.innerHTML = `<div>Crash ID: ${info.errorId} <button id="retry">Retry</button></div>`;
478
+ * document.getElementById('retry')?.addEventListener('click', () => guard.recover());
479
+ * } else {
480
+ * renderApp(container);
481
+ * }
482
+ * });
483
+ */
484
+ declare class NexusGuardCore {
485
+ private _error;
486
+ private _errorInfo;
487
+ private _listeners;
488
+ private readonly _tracker?;
489
+ private readonly _onError?;
490
+ private readonly _tags?;
491
+ private readonly _generateErrorId;
492
+ constructor(options?: NexusGuardCoreOptions);
493
+ /** Whether the guard is currently in an error state. */
494
+ get hasError(): boolean;
495
+ /** The captured Error instance, or null if healthy. */
496
+ get currentError(): Error | null;
497
+ /** Full normalized error info for rendering fallback UI, or null if healthy. */
498
+ get errorInfo(): NexusErrorInfo | null;
499
+ /**
500
+ * Captures an error thrown within the guarded scope.
501
+ * Dispatches to subscribers, calls onError callback, and sends to tracker if configured.
502
+ */
503
+ capture(error: unknown, context?: {
504
+ componentStack?: string;
505
+ tags?: Record<string, string>;
506
+ }): NexusErrorInfo;
507
+ /**
508
+ * Recovers from the error state back to normal execution.
509
+ * Clears the current error and notifies all subscribers with null.
510
+ */
511
+ recover(): void;
512
+ /** Alias for recover(). */
513
+ reset(): void;
514
+ /**
515
+ * Subscribes to guard state changes (error captured or recovered).
516
+ * @param listener Callback receiving NexusErrorInfo or null on recovery.
517
+ * @returns Unsubscribe function.
518
+ */
519
+ subscribe(listener: (info: NexusErrorInfo | null) => void): () => void;
520
+ private _notify;
521
+ }
522
+
523
+ /**
524
+ * @fileoverview Web Vitals and Long Task performance observer for the Nexus Tracker.
525
+ * Captures LCP, CLS, FID, TTFB, INP, FCP as breadcrumbs or full error events.
526
+ * Uses the native PerformanceObserver API — no external dependencies.
527
+ * @module @nexus/sdk-tracker/performance
528
+ */
529
+
530
+ /**
531
+ * Attaches Web Vitals observers to the tracker instance.
532
+ * Records metrics as breadcrumbs (default) or as full error events (when `captureAsEvents: true`).
533
+ * Also observes long tasks above the configured threshold.
534
+ *
535
+ * Must be called in a browser context after `DOMContentLoaded`.
536
+ * Safe to call in Next.js client components and SPA route entry points.
537
+ *
538
+ * @param tracker - The initialized NexusTrackerClient instance.
539
+ * @param options - Observer configuration.
540
+ * @returns Cleanup function — call on component unmount or SDK destroy.
541
+ *
542
+ * @example
543
+ * // Record all vitals as breadcrumbs
544
+ * const cleanup = attachWebVitals(tracker);
545
+ *
546
+ * @example
547
+ * // Record poor vitals as full events with console output
548
+ * const cleanup = attachWebVitals(tracker, {
549
+ * poorRatingOnly: true,
550
+ * captureAsEvents: true,
551
+ * vitals: ['LCP', 'CLS', 'INP'],
552
+ * });
553
+ *
554
+ * @example
555
+ * // Next.js App Router — attach in a Client Component effect
556
+ * useEffect(() => {
557
+ * return attachWebVitals(tracker, { poorRatingOnly: true });
558
+ * }, []);
559
+ */
560
+ declare function attachWebVitals(tracker: NexusTrackerClient, options?: PerformanceVitalsOptions): () => void;
561
+
562
+ export { BreadcrumbManager, type INexusTrackerClient, NexusGuardCore, type NexusGuardCoreOptions, NexusTrackerClient, type NexusTrackerOptions, Transport, type TransportOptions, attachWebVitals, computeFingerprint, parseStackTrace, sanitizeObject, sanitizeUrl };