diff --git a/dist/core/Client.d.ts b/dist/core/Client.d.ts
index 69e9164..35f9bd3 100644
--- a/dist/core/Client.d.ts
+++ b/dist/core/Client.d.ts
@@ -1,6 +1,5 @@
-import type { LightConfig, LightClient, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb } from '../types';
+import type { LightConfig, LightClient, LightPlugin, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb } from '../types';
declare class Client implements LightClient {
- config: LightConfig;
dsn: DSNInfo;
private configManager;
private eventBus;
@@ -8,8 +7,9 @@ declare class Client implements LightClient {
private reporter;
private pluginManager;
private breadcrumbs;
- private maxBreadcrumbs;
private enabled;
+ private get maxBreadcrumbs();
+ get config(): LightConfig;
constructor(config: LightConfig);
init(): void;
private flushEvents;
@@ -23,7 +23,6 @@ declare class Client implements LightClient {
type: string;
}): void;
private buildErrorEvent;
- private parseStackTrace;
setUser(user: UserInfo | null): void;
setTag(key: string, value: string): void;
setTags(tags: Record): void;
@@ -32,7 +31,7 @@ declare class Client implements LightClient {
flush(): Promise;
disable(): void;
enable(): void;
- use(plugin: unknown): void;
+ use(plugin: LightPlugin): void;
destroy(): void;
}
export default Client;
diff --git a/dist/core/ConfigManager.d.ts b/dist/core/ConfigManager.d.ts
index 812becc..eac58d8 100644
--- a/dist/core/ConfigManager.d.ts
+++ b/dist/core/ConfigManager.d.ts
@@ -13,7 +13,6 @@ export declare class ConfigManager {
setTags(tags: Record): void;
setTag(key: string, value: string): void;
setExtra(key: string, value: unknown): void;
- shouldSample(): boolean;
isIgnoredError(message: string): boolean;
/**
* 获取编码后的环境信息(延迟初始化,只在首次上报时计算)
@@ -28,6 +27,7 @@ export declare class ConfigManager {
applyToEvent(event: SentryEvent, includeFullContext?: boolean): SentryEvent;
/**
* 应用上下文分层策略:根据事件级别裁剪堆栈和 breadcrumbs
+ * 注意:此方法不修改原始 event 对象,返回裁剪后的副本
*/
private applyContextLevel;
}
diff --git a/dist/core/EventQueue.d.ts b/dist/core/EventQueue.d.ts
index 40edad0..867fcf6 100644
--- a/dist/core/EventQueue.d.ts
+++ b/dist/core/EventQueue.d.ts
@@ -9,6 +9,10 @@ export declare class EventQueue {
private flushCallback;
private syncFlushCallback;
private lastFlushTime;
+ private isFlushing;
+ private onVisibilityChange?;
+ private onBeforeUnload?;
+ private onPagehide?;
private dedupeMap;
private pendingCounts;
private errorRateWindow;
@@ -20,6 +24,7 @@ export declare class EventQueue {
private setupVisibilityListener;
private checkInfiniteLoop;
enqueue(event: SentryEvent): void;
+ private cleanupExpiredDedupeEntries;
private buildCountEvents;
private getDedupeKey;
flush(): Promise;
diff --git a/dist/core/Reporter.d.ts b/dist/core/Reporter.d.ts
index 9d91cd9..2a91c3a 100644
--- a/dist/core/Reporter.d.ts
+++ b/dist/core/Reporter.d.ts
@@ -35,7 +35,6 @@ export declare class Reporter {
*/
private stripSharedFields;
private getEnvelopeType;
- private generateEventId;
private send;
private sendSync;
private sendViaImage;
diff --git a/dist/index.cjs.js b/dist/index.cjs.js
index 801442c..f744556 100644
--- a/dist/index.cjs.js
+++ b/dist/index.cjs.js
@@ -1,4 +1,3 @@
-"use strict";var T=Object.defineProperty;var le=Object.getOwnPropertyDescriptor;var pe=Object.getOwnPropertyNames;var de=Object.prototype.hasOwnProperty;var fe=(s,e)=>{for(var t in e)T(s,t,{get:e[t],enumerable:!0})},he=(s,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of pe(e))!de.call(s,r)&&r!==t&&T(s,r,{get:()=>e[r],enumerable:!(n=le(e,r))||n.enumerable});return s};var ge=s=>he(T({},"__esModule",{value:!0}),s);var Re={};fe(Re,{BehaviorPlugin:()=>$,Client:()=>B,ErrorPlugin:()=>O,NetworkPlugin:()=>U,OfflinePlugin:()=>K,PerformancePlugin:()=>F,addBreadcrumb:()=>oe,captureEvent:()=>te,captureException:()=>Y,captureMessage:()=>ee,default:()=>xe,disable:()=>ce,enable:()=>ue,flush:()=>ae,getClient:()=>Z,init:()=>J,setExtra:()=>se,setTag:()=>re,setTags:()=>ie,setUser:()=>ne});module.exports=ge(Re);function A(s){try{let e=s.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);if(!e)throw new Error("Invalid DSN format");let[,t,n,r,i]=e;return{protocol:t,publicKey:n,host:r,projectId:i}}catch(e){throw new Error(`Failed to parse DSN: ${e.message}`)}}function b(s){return`${s.protocol}://${s.host}/${s.projectId}/envelope/`}function p(){return Date.now()}function v(s){return s!==null&&typeof s=="object"&&!Array.isArray(s)}function C(s){let e=0;for(let t=0;t`${i.filename}:${i.lineno||0}`).join("|"),r=e.replace(/['"]?\d+['"]?/g,"").replace(/\s+/g," ").trim();return C(`${s}:${r}:${n}`)}function me(s){let e=[{name:"WeChat",pattern:/MicroMessenger\/([\d.]+)/i},{name:"QQ Browser",pattern:/QIBrowser\/([\d.]+)/i},{name:"UC Browser",pattern:/UCBrowser\/([\d.]+)/i},{name:"Edge",pattern:/Edg\/([\d.]+)/i},{name:"Edge",pattern:/Edge\/([\d.]+)/i},{name:"Opera",pattern:/OPR\/([\d.]+)/i},{name:"Opera",pattern:/Opera\/([\d.]+)/i},{name:"Firefox",pattern:/Firefox\/([\d.]+)/i},{name:"Safari",pattern:/Version\/([\d.]+).*Safari/i},{name:"Chrome",pattern:/Chrome\/([\d.]+)/i},{name:"IE",pattern:/MSIE ([\d.]+)/i},{name:"IE",pattern:/Trident.*rv:([\d.]+)/i}];for(let t of e){let n=s.match(t.pattern);if(n){let r=n[1]||"",i=r.split(".")[0]||"";return{name:t.name,version:r,major:i}}}return{name:"Unknown",version:"",major:""}}function ve(s){let e=[{name:"Windows 11",pattern:/Windows NT 10\.0.*Win64.*x64/i},{name:"Windows 10",pattern:/Windows NT 10\.0/i},{name:"Windows 8.1",pattern:/Windows NT 6\.3/i},{name:"Windows 8",pattern:/Windows NT 6\.2/i},{name:"Windows 7",pattern:/Windows NT 6\.1/i},{name:"Windows Vista",pattern:/Windows NT 6\.0/i},{name:"Windows XP",pattern:/Windows NT 5\.1/i},{name:"macOS",pattern:/Mac OS X ([\d_.]+)/i},{name:"iOS",pattern:/OS ([\d_.]+) like Mac OS X/i},{name:"Android",pattern:/Android ([\d.]+)/i},{name:"Android",pattern:/Android/i},{name:"Linux",pattern:/Linux/i}];for(let t of e){let n=s.match(t.pattern);if(n){let r=n[1]||"";return r&&(r=r.replace(/_/g,".")),{name:t.name,version:r}}}return{name:"Unknown",version:""}}function ye(s){let e=/Mobile|Android|iPhone|iPod|BlackBerry|Windows Phone|Opera Mini/i.test(s),t=/Tablet|iPad|PlayBook|Kindle|Silk/i.test(s),n="desktop";t?n="tablet":e&&(n="mobile");let r="",i="",o=s.match(/iPhone/i),a=s.match(/iPad/i),c=s.match(/SM-([A-Z0-9]+)/i),u=s.match(/Pixel ([0-9A-Z]+)/i),d=s.match(/HUAWEI ([A-Z0-9]+)/i);return o?(r="Apple",i="iPhone"):a?(r="Apple",i="iPad"):c?(r="Samsung",i=c[1]):u?(r="Google",i=`Pixel ${u[1]}`):d&&(r="Huawei",i=d[1]),{type:n,vendor:r,model:i}}function X(){let s=typeof navigator!="undefined"?navigator.userAgent:"",e=me(s),t=ve(s),n=ye(s);return{browser:e,os:t,device:n,url:typeof location!="undefined"?location.href:"",referrer:typeof document!="undefined"?document.referrer:"",title:typeof document!="undefined"?document.title:"",language:typeof navigator!="undefined"?navigator.language:"",user_agent:s,screen_width:typeof screen!="undefined"?screen.width:0,screen_height:typeof screen!="undefined"?screen.height:0,viewport_width:typeof window!="undefined"?window.innerWidth:0,viewport_height:typeof window!="undefined"?window.innerHeight:0}}function g(){return typeof location!="undefined"?location.href:""}function z(){return typeof document!="undefined"?document.referrer:""}function M(s){if(!s)return s;try{let e=new URL(s),t=["password","passwd","pwd","token","access_token","refresh_token","api_key","apikey","key","secret","private_key","code","auth","id_token","idToken","session","session_id"],n=e.searchParams,r=!1;for(let i of Array.from(n.keys())){let o=i.toLowerCase().replace(/[-_]/g,"_");t.some(a=>o.includes(a))&&(n.set(i,"[Filtered]"),r=!0)}return r?e.toString():s}catch(e){return s}}function w(s,e=[]){return!e||e.length===0?!1:e.some(t=>{if(typeof t=="string"){if(t.includes("*")){let n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(n).test(s)}return s.includes(t)}return t.test(s)})}var be={Chrome:"c",Safari:"s",Firefox:"f",Edge:"e",Opera:"o",IE:"i",WeChat:"w","QQ Browser":"q","UC Browser":"u","Mobile Chrome":"cm","Mobile Safari":"sm"},Ee={"Windows 11":"w11","Windows 10":"w10","Windows 8.1":"w81","Windows 8":"w8","Windows 7":"w7","Windows Vista":"wv","Windows XP":"wxp",macOS:"m",iOS:"i",Android:"a",Linux:"l"};function I(){let s=X(),e=be[s.browser.name]||s.browser.name.toLowerCase(),t=Ee[s.os.name]||s.os.name.toLowerCase();return{browser:{name:e,version:s.browser.major},os:{name:t,version:s.os.version.split(".")[0]},device:{type:s.device.type,brand:s.device.vendor||void 0,model:s.device.model||void 0}}}function V(){let s=X(),e=`${s.browser.name}|${s.browser.major}|${s.os.name}|${s.os.version.split(".")[0]}|${s.device.type}|${s.device.vendor}|${s.device.model}|${s.language}`;return C(e).substring(0,16)}var we={enabled:!0,sampleRate:1,maxQueueSize:100,flushInterval:5e3,maxRetries:3,retryDelay:1e3,environment:"production",contextLevel:{fatal:{maxStackFrames:5,maxBreadcrumbs:20},error:{maxStackFrames:5,maxBreadcrumbs:10},warning:{maxStackFrames:3,maxBreadcrumbs:5},info:{maxStackFrames:0,maxBreadcrumbs:0},debug:{maxStackFrames:0,maxBreadcrumbs:0}}},S=class{constructor(e){this.contexts=null;this.contextId=null;this.contextReported=!1;this.config={...we,...e}}markContextReported(){this.contextReported=!0}get(e){return this.config[e]}getAll(){return{...this.config}}set(e,t){this.config[e]=t}setUser(e){this.config.user=e||void 0}setTags(e){this.config.tags={...this.config.tags||{},...e}}setTag(e,t){this.config.tags||(this.config.tags={}),this.config.tags[e]=t}setExtra(e,t){this.config.extra||(this.config.extra={}),this.config.extra[e]=t}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:1;return e>=1?!0:e<=0?!1:Math.random()typeof n=="string"?e.includes(n):n.test(e))}getEncodedContext(){return this.contexts||(this.contexts=I()),this.contexts}applyToEvent(e,t=!0){var n,r,i,o;return this.config.release&&(e.release=this.config.release),this.config.environment&&(e.environment=this.config.environment),this.config.user&&(e.user=this.config.user),this.config.tags&&(e.tags={...this.config.tags,...e.tags||{}}),e.request?e.request.url||(e.request.url=g()):e.request={url:g(),referrer:z()},this.contextId||(this.contextId=V()),e.context_id=this.contextId,this.applyContextLevel(e),t&&!this.contextReported&&(this.contexts||(this.contexts=I()),e.contexts=this.contexts,e.env={b:(n=this.contexts.browser)==null?void 0:n.name,bv:(r=this.contexts.browser)==null?void 0:r.version,os:(i=this.contexts.os)==null?void 0:i.name,osv:(o=this.contexts.os)==null?void 0:o.version}),e}applyContextLevel(e){var i,o,a,c;let t=this.config.contextLevel;if(!t)return;let n=e.level||"error",r=t[n]||t.error;(o=(i=e.exception)==null?void 0:i.stacktrace)!=null&&o.frames&&r.maxStackFrames>0?e.exception.stacktrace.frames=e.exception.stacktrace.frames.slice(0,r.maxStackFrames):r.maxStackFrames===0&&((c=(a=e.exception)==null?void 0:a.stacktrace)!=null&&c.frames)&&delete e.exception.stacktrace,e.breadcrumbs&&r.maxBreadcrumbs>0?e.breadcrumbs=e.breadcrumbs.slice(-r.maxBreadcrumbs):r.maxBreadcrumbs===0&&delete e.breadcrumbs}};var x=class{constructor(){this.handlers=new Map}on(e,t){this.handlers.has(e)||this.handlers.set(e,[]),this.handlers.get(e).push(t)}off(e,t){let n=this.handlers.get(e);if(n){let r=n.indexOf(t);r>-1&&n.splice(r,1)}}emit(e,...t){let n=this.handlers.get(e);if(n)for(let r of n)try{r(...t)}catch(i){console.error("[LightSDK] EventBus handler error:",i)}}once(e,t){let n=(...r)=>{this.off(e,n),t(...r)};this.on(e,n)}destroy(){this.handlers.clear()}};var R=class{constructor(e,t,n,r,i){this.queue=[];this.timer=null;this.lastFlushTime=0;this.dedupeMap=new Map;this.pendingCounts=new Map;this.errorRateWindow={};this.paused=!1;this.pauseTimer=null;this.maxSize=e,this.flushInterval=t,this.eventBus=n,this.flushCallback=r,this.syncFlushCallback=i}start(){this.startTimer(),this.setupVisibilityListener()}startTimer(){this.timer||(this.timer=setInterval(()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flush()},this.flushInterval))}setupVisibilityListener(){typeof document!="undefined"&&document.addEventListener("visibilitychange",()=>{document.hidden&&(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}),typeof window!="undefined"&&(window.addEventListener("beforeunload",()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}),window.addEventListener("pagehide",()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}))}checkInfiniteLoop(e){let t=p(),n=t-1e3;this.errorRateWindow[e]||(this.errorRateWindow[e]=[]);let r=this.errorRateWindow[e];for(r.push(t);r.length>0&&r[0]10?(this.paused||(console.warn("[LightSDK] Infinite loop detected, pausing SDK for 60s",{fingerprint:e,count:r.length}),this.eventBus.emit("error",new Error("Infinite loop detected")),this.paused=!0,this.pauseTimer&&clearTimeout(this.pauseTimer),this.pauseTimer=setTimeout(()=>{this.paused=!1,this.errorRateWindow={},console.info("[LightSDK] Resumed after infinite loop detection")},6e4)),!0):!1}enqueue(e){if(this.paused)return;let t=this.getDedupeKey(e);if(t){if(this.checkInfiniteLoop(t))return;let n=p(),r=this.dedupeMap.get(t);if(r)if(n-r.lastTime<6e4){if(r.count>=3){if(!this.pendingCounts.has(t))this.pendingCounts.set(t,{count:1,ts_start:n,ts_end:n});else{let i=this.pendingCounts.get(t);i.count++,i.ts_end=n}return}r.count++,r.lastTime=n}else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!0}),this.pendingCounts.delete(t);else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!1})}this.queue.length>=this.maxSize&&this.flush(),this.queue.push(e),this.eventBus.emit("event",e),this.queue.length>=this.maxSize&&this.flush()}buildCountEvents(){let e=[];for(let[t,n]of this.pendingCounts.entries()){let r={type:"count",fingerprint:t,count:n.count,ts_start:n.ts_start,ts_end:n.ts_end,timestamp:p(),level:"info"};e.push(r)}return this.pendingCounts.clear(),e}getDedupeKey(e){if(e.type==="error"){let t=e;return t.fingerprint||t.message}return null}async flush(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=p(),this.eventBus.emit("report",n);try{await this.flushCallback(n),this.eventBus.emit("reported",n)}catch(r){throw e.forEach(i=>this.queue.unshift(i)),r}}flushSync(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=p(),this.eventBus.emit("report",n);try{this.syncFlushCallback(n),this.eventBus.emit("reported",n)}catch(r){console.error("[LightSDK] Sync flush failed",r)}}size(){return this.queue.length}destroy(){this.timer&&(clearInterval(this.timer),this.timer=null),this.pauseTimer&&(clearTimeout(this.pauseTimer),this.pauseTimer=null),this.dedupeMap.clear(),this.pendingCounts.clear(),this.errorRateWindow={}}};var Q={event_id:"eid",timestamp:"ts",start_timestamp:"sts",type:"t",level:"l",message:"m",platform:"p",release:"r",environment:"e",fingerprint:"fp",context_id:"cid",user:"u",tags:"tg",exception:"ex",request:"req",contexts:"ctx",breadcrumbs:"bc",transaction:"tx",duration:"d",spans:"sp",extra:"xt"},Ne=Object.fromEntries(Object.entries(Q).map(([s,e])=>[e,s]));function G(s){if(!s||typeof s!="object")return s;let e={};for(let[t,n]of Object.entries(s)){let r=Q[t]||t;e[r]=n}return e}var L=class{constructor(e,t,n){this.useShortFields=!0;this.dsn=e,this.maxRetries=t,this.retryDelay=n}async report(e){if(e.length===0)return;let t=this.buildEnvelope(e),n=0;for(;n<=this.maxRetries;)try{await this.send(t,!1);return}catch(r){let i=r.status;if(i&&i>=400&&i<500||(n++,n>this.maxRetries))throw r;await this.delay(this.retryDelay*Math.pow(2,n-1))}}reportSync(e){if(e.length===0)return;let t=this.buildEnvelope(e);try{if(this.sendSync(t))return}catch(n){}this.sendViaImage(t)}buildEnvelope(e){let t=this.extractSharedMeta(e),n={event_id:this.generateEventId(),sent_at:new Date().toISOString(),meta:t};this.useShortFields&&(n._sf=1);let r=this.applyRelativeTimestamps(e,n),o=[JSON.stringify(n)];for(let a of r){let c=this.stripSharedFields(a,t);this.useShortFields&&(c=G(c));let u=JSON.stringify(c),d=JSON.stringify({type:this.getEnvelopeType(a),length:u.length});o.push(d,u)}return o.join(`
-`)}applyRelativeTimestamps(e,t){if(e.length<=1)return e;let n=[],r=e[0].timestamp?new Date(e[0].timestamp).getTime():Date.now();t._rt=1,t._bt=r;for(let i=0;io.release===t.release)&&(n.release=t.release),t.environment&&e.every(o=>o.environment===t.environment)&&(n.environment=t.environment),t.user&&e.every(o=>JSON.stringify(o.user)===JSON.stringify(t.user))&&(n.user=t.user);let r=t.env;return r&&(n.env=r),n}stripSharedFields(e,t){let n={...e};return t.release&&n.release===t.release&&delete n.release,t.environment&&n.environment===t.environment&&delete n.environment,t.user&&JSON.stringify(n.user)===JSON.stringify(t.user)&&delete n.user,t.env&&delete n.env,n}getEnvelopeType(e){switch(e.type){case"error":return"event";case"performance":return"transaction";default:return e.type}}generateEventId(){return"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".replace(/[x]/g,()=>(Math.random()*16|0).toString(16))}async send(e,t=!1){let n=b(this.dsn);if(navigator.sendBeacon)try{let r=new Blob([e],{type:"application/x-sentry-envelope"});if(navigator.sendBeacon(n,r))return}catch(r){}if(typeof fetch=="function")try{let r=await fetch(n,{method:"POST",body:e,headers:{"Content-Type":"application/x-sentry-envelope"},keepalive:!0});if(r.ok)return;let i=new Error(`HTTP ${r.status}`);throw i.status=r.status,i}catch(r){throw r}if(typeof XMLHttpRequest!="undefined")return new Promise((r,i)=>{let o=new XMLHttpRequest;o.open("POST",n,!t),o.setRequestHeader("Content-Type","application/x-sentry-envelope"),o.onload=()=>{if(o.status>=200&&o.status<300)r();else{let a=new Error(`HTTP ${o.status}`);a.status=o.status,i(a)}},o.onerror=()=>i(new Error("Network error")),o.send(e)});throw new Error("No transport available")}sendSync(e){let t=b(this.dsn);if(navigator.sendBeacon)try{let n=new Blob([e],{type:"application/x-sentry-envelope"});return navigator.sendBeacon(t,n)}catch(n){return!1}if(typeof XMLHttpRequest!="undefined")try{let n=new XMLHttpRequest;return n.open("POST",t,!1),n.setRequestHeader("Content-Type","application/x-sentry-envelope"),n.send(e),n.status>=200&&n.status<300}catch(n){return!1}return!1}sendViaImage(e){try{let t=b(this.dsn),n=new Image,r=encodeURIComponent(btoa(e));return n.src=t+"&sentry_data="+r.substring(0,2e3),!0}catch(t){return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};var k=class{constructor(e){this.plugins=new Map;this.client=e}add(e){if(!this.plugins.has(e.name)){this.plugins.set(e.name,e);try{e.setup(this.client)}catch(t){console.error(`[LightSDK] Plugin "${e.name}" setup error:`,t)}}}remove(e){let t=this.plugins.get(e);if(t){if(t.destroy)try{t.destroy()}catch(n){console.error(`[LightSDK] Plugin "${e}" destroy error:`,n)}this.plugins.delete(e)}}get(e){return this.plugins.get(e)}has(e){return this.plugins.has(e)}applyBeforeReport(e){let t=e;for(let n of this.plugins.values())if(n.beforeReport)try{let r=n.beforeReport(t);if(r===null)return null;t=r}catch(r){console.error(`[LightSDK] Plugin "${n.name}" beforeReport error:`,r)}return t}applyAfterReport(e){for(let t of this.plugins.values())if(t.afterReport)try{t.afterReport(e)}catch(n){console.error(`[LightSDK] Plugin "${t.name}" afterReport error:`,n)}}destroy(){for(let e of this.plugins.values())if(e.destroy)try{e.destroy()}catch(t){console.error(`[LightSDK] Plugin "${e.name}" destroy error:`,t)}this.plugins.clear()}};var _=class{constructor(e){this.breadcrumbs=[];this.maxBreadcrumbs=20;this.enabled=!0;var t,n,r,i;if(!e.dsn)throw new Error("DSN is required");this.dsn=A(e.dsn),this.configManager=new S(e),this.config=this.configManager.getAll(),this.eventBus=new x,this.reporter=new L(this.dsn,(t=e.maxRetries)!=null?t:3,(n=e.retryDelay)!=null?n:1e3),this.pluginManager=new k(this),this.queue=new R((r=e.maxQueueSize)!=null?r:100,(i=e.flushInterval)!=null?i:5e3,this.eventBus,async o=>this.flushEvents(o),o=>this.syncFlushEvents(o)),this.enabled=e.enabled!==!1}init(){this.enabled&&(this.queue.start(),this.emit("ready"))}async flushEvents(e){let t=[];for(let i of e){let o=this.pluginManager.applyBeforeReport(i);o&&t.push(o)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let o=this.configManager.applyToEvent(i,n);n=!1;let a=this.config.get("beforeSend")?this.config.get("beforeSend")(o):o;a&&r.push(a)}if(r.length!==0){await this.reporter.report(r),this.configManager.markContextReported();for(let i of r)this.pluginManager.applyAfterReport(i)}}syncFlushEvents(e){let t=[];for(let i of e){let o=this.pluginManager.applyBeforeReport(i);o&&t.push(o)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let o=this.configManager.applyToEvent(i,n);n=!1;let a=this.config.get("beforeSend")?this.config.get("beforeSend")(o):o;a&&r.push(a)}r.length!==0&&(this.reporter.reportSync(r),this.configManager.markContextReported())}on(e,t){this.eventBus.on(e,t)}off(e,t){this.eventBus.off(e,t)}emit(e,...t){this.eventBus.emit(e,...t)}captureException(e){if(!this.enabled||!this.configManager.shouldSample())return;let t=this.buildErrorEvent(e);this.configManager.isIgnoredError(t.message)||(t.breadcrumbs=[...this.breadcrumbs],this.queue.enqueue(t))}captureMessage(e,t="info"){if(!this.enabled||!this.configManager.shouldSample())return;let n={type:"error",level:t,message:e,timestamp:p(),breadcrumbs:[...this.breadcrumbs]};this.configManager.isIgnoredError(e)||this.queue.enqueue(n)}captureEvent(e){if(!this.enabled||!this.configManager.shouldSample())return;let t={timestamp:p(),level:"info",...e};this.queue.enqueue(t)}buildErrorEvent(e){let t=p();if(e instanceof Error){let r=this.parseStackTrace(e.stack),i=E(e.name,e.message,r);return{type:"error",level:"error",message:e.message,timestamp:t,exception:{type:e.name,value:e.message,stacktrace:{frames:r.slice(0,5)}},fingerprint:i}}return{type:"error",level:"error",message:String(e),timestamp:t}}parseStackTrace(e){if(!e)return[];let t=[],n=e.split(`
-`);for(let r of n){let i=r.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(i){let[,o,a,c,u]=i;t.push({filename:a,function:o,lineno:parseInt(c,10),colno:parseInt(u,10),in_app:!a.includes("node_modules")})}else{let o=r.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(o){let[,a,c,u]=o;t.push({filename:a,lineno:parseInt(c,10),colno:parseInt(u,10),in_app:!a.includes("node_modules")})}}}return t.reverse()}setUser(e){this.configManager.setUser(e)}setTag(e,t){this.configManager.setTag(e,t)}setTags(e){this.configManager.setTags(e)}setExtra(e,t){this.configManager.setExtra(e,t)}addBreadcrumb(e){let t={...e,timestamp:p()};this.breadcrumbs.push(t),this.breadcrumbs.length>this.maxBreadcrumbs&&this.breadcrumbs.shift()}async flush(){await this.queue.flush()}disable(){this.enabled=!1}enable(){this.enabled=!0}use(e){e&&typeof e=="object"&&"name"in e&&"setup"in e&&this.pluginManager.add(e)}destroy(){this.queue.destroy(),this.pluginManager.destroy(),this.eventBus.destroy(),this.breadcrumbs=[]}},B=_;var D=class{constructor(){this.name="error";this.version="1.0.0";this.config={maxStackFrames:5,captureNodeModules:!1,captureColumn:!0,relativePathOnly:!1}}setup(e){this.client=e,this.loadConfig(),this.setupGlobalError(),this.setupUnhandledRejection(),this.setupResourceError()}loadConfig(){let e=this.client.config;e&&v(e.error)&&(this.config={...this.config,...e.error}),e.ignoreErrors&&(this.config.ignoreErrors=[...this.config.ignoreErrors||[],...e.ignoreErrors]),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnoreError(e){return!this.config.ignoreErrors||!e?!1:this.config.ignoreErrors.some(t=>typeof t=="string"?e.includes(t):t.test(e))}shouldIgnoreScriptUrl(e){return w(e,this.config.ignoreUrls)}setupGlobalError(){if(typeof window=="undefined")return;let e=window.onerror;window.onerror=(t,n,r,i,o)=>{try{e&&e.call(window,t,n,r,i,o);let a=this.buildErrorEvent(o||t,n,r,i);a&&this.client.captureEvent(a)}catch(a){console.error("[LightSDK] ErrorPlugin global error handler error:",a)}return!1}}setupUnhandledRejection(){typeof window!="undefined"&&window.addEventListener("unhandledrejection",e=>{try{let t=e.reason,n=this.buildErrorEvent(t);n&&(n.type==="error"&&(n.tags={...n.tags||{},unhandled:"true",promise:"true"}),this.client.captureEvent(n))}catch(t){console.error("[LightSDK] ErrorPlugin unhandledrejection handler error:",t)}})}setupResourceError(){typeof window!="undefined"&&window.addEventListener("error",e=>{var i;let t=e.target;if(!t)return;let n=(i=t.tagName)==null?void 0:i.toLowerCase(),r=t.src||t.href;if(n&&r&&["img","script","link","audio","video"].includes(n))try{this.client.captureEvent({type:"error",level:"warning",message:`Resource load failed: ${n} ${r}`,timestamp:p(),tags:{resource_type:n,resource_url:r},exception:{type:"ResourceError",value:`Failed to load ${n} resource`}})}catch(o){console.error("[LightSDK] ErrorPlugin resource error handler error:",o)}},!0)}buildErrorEvent(e,t,n,r){let i=p();if(e instanceof Error){if(this.shouldIgnoreError(e.message))return null;let c=this.parseStackTrace(e.stack),u=E(e.name,e.message,c);return{type:"error",level:"error",message:e.message,timestamp:i,exception:{type:e.name,value:e.message,stacktrace:{frames:c.slice(0,this.config.maxStackFrames||5)}},fingerprint:u}}let o=String(e);if(this.shouldIgnoreError(o))return null;let a=[];return t&&a.push({filename:t,lineno:n,colno:this.config.captureColumn?r:void 0,in_app:!0}),{type:"error",level:"error",message:o,timestamp:i,exception:{type:"Error",value:o,stacktrace:{frames:a}}}}parseStackTrace(e){if(!e)return[];let t=[],n=e.split(`
-`),r=typeof location!="undefined"?`${location.protocol}//${location.host}`:"";for(let i of n){let o=i.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(o){let[,a,c,u,d]=o,f=c.includes("node_modules");if(!this.config.captureNodeModules&&f)continue;let h=c;this.config.relativePathOnly&&r&&c.startsWith(r)&&(h=c.substring(r.length)),t.push({filename:h,function:a,lineno:parseInt(u,10),colno:this.config.captureColumn?parseInt(d,10):void 0,in_app:!f})}else{let a=i.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(a){let[,c,u,d]=a,f=c.includes("node_modules");if(!this.config.captureNodeModules&&f)continue;let h=c;this.config.relativePathOnly&&r&&c.startsWith(r)&&(h=c.substring(r.length)),t.push({filename:h,lineno:parseInt(u,10),colno:this.config.captureColumn?parseInt(d,10):void 0,in_app:!f})}}}return t.reverse()}destroy(){}},O=D;var N=class{constructor(){this.name="performance";this.version="1.0.0";this.config={sampleRate:.1,captureLongTasks:!0,captureResources:!1,resourceSampleRate:.01,longTaskSampleRate:.05,captureNavigation:!0,captureFP:!0}}setup(e){this.client=e,this.loadConfig(),this.shouldSample("default")&&(this.observeWebVitals(),this.observeLongTasks(),this.observeNavigation(),this.observeResources())}loadConfig(){let e=this.client.config;e&&v(e.performance)&&(this.config={...this.config,...e.performance})}shouldSample(e="default"){var n,r,i;let t=(n=this.config.sampleRate)!=null?n:.1;return e==="resource"?t=(r=this.config.resourceSampleRate)!=null?r:.01:e==="longtask"&&(t=(i=this.config.longTaskSampleRate)!=null?i:.05),t>=1?!0:t<=0?!1:Math.random(){let n=t.getEntriesByName("first-paint");n.length>0&&this.reportMetric("FP",n[0].startTime,"ms")}).observe({type:"paint",buffered:!0})}catch(e){}}observeLCP(){try{new PerformanceObserver(t=>{let n=t.getEntries(),r=n[n.length-1];if(r){let i=r.renderTime||r.loadTime||r.startTime;this.reportMetric("LCP",i,"ms")}}).observe({type:"largest-contentful-paint",buffered:!0})}catch(e){}}observeFID(){try{new PerformanceObserver(t=>{let n=t.getEntries();if(n.length>0){let r=n[0],i=r.processingStart-r.startTime;this.reportMetric("FID",i,"ms")}}).observe({type:"first-input",buffered:!0})}catch(e){}}observeCLS(){try{let e=0,t=0,n=[];new PerformanceObserver(i=>{let o=i.getEntries();for(let a of o)if(!a.hadRecentInput){let c=n[0],u=n[n.length-1];t&&a.startTime-u.endTime<1e3&&a.startTime-c.startTime<5e3?(t+=a.value||0,n.push(a)):(t=a.value||0,n=[a]),t>e&&(e=t,this.reportMetric("CLS",e,""))}}).observe({type:"layout-shift",buffered:!0})}catch(e){}}observeFCP(){try{new PerformanceObserver(t=>{let n=t.getEntriesByName("first-contentful-paint");n.length>0&&this.reportMetric("FCP",n[0].startTime,"ms")}).observe({type:"paint",buffered:!0})}catch(e){}}observeTTFB(){try{new PerformanceObserver(t=>{let n=t.getEntriesByType("navigation");if(n.length>0){let r=n[0],i=r.responseStart-r.requestStart;this.reportMetric("TTFB",Math.max(0,i),"ms")}}).observe({type:"navigation",buffered:!0})}catch(e){}}observeLongTasks(){if(this.config.captureLongTasks)try{new PerformanceObserver(t=>{let n=t.getEntries();for(let r of n)this.shouldSample("longtask")&&this.reportMetric("longtask",r.duration,"ms")}).observe({type:"longtask",buffered:!0})}catch(e){}}observeNavigation(){this.config.captureNavigation&&typeof performance!="undefined"&&window.addEventListener("load",()=>{setTimeout(()=>{let e=performance.timing;if(!e)return;let t=e.navigationStart,n={dom_ready:e.domContentLoadedEventEnd-t,load_time:e.loadEventEnd-t,dns:e.domainLookupEnd-e.domainLookupStart,tcp:e.connectEnd-e.connectStart,ssl:e.secureConnectionStart>0?e.connectEnd-e.secureConnectionStart:0,ttfb:e.responseStart-e.requestStart,download:e.responseEnd-e.responseStart,dom_parse:e.domInteractive-e.responseEnd};for(let[r,i]of Object.entries(n))i>0&&this.reportMetric(r,i,"ms")},0)})}observeResources(){if(this.config.captureResources&&typeof performance!="undefined")try{new PerformanceObserver(t=>{if(!this.shouldSample("resource"))return;let n=t.getEntriesByType("resource");for(let r of n)r.duration>1e3&&this.reportMetric("resource_slow",r.duration,"ms",{resource_name:r.name.substring(0,200),resource_type:r.initiatorType})}).observe({type:"resource",buffered:!0})}catch(e){}}getRating(e,t){let r={LCP:{good:2500,poor:4e3},FCP:{good:1800,poor:3e3},FP:{good:1800,poor:3e3},FID:{good:100,poor:300},CLS:{good:.1,poor:.25},TTFB:{good:800,poor:1800}}[e];return!r||t<=r.good?"good":t<=r.poor?"needs-improvement":"poor"}reportMetric(e,t,n,r={}){let i=this.getRating(e,t),o={type:"performance",level:"info",metric:e,value:Math.round(t*100)/100,unit:n,rating:i,timestamp:p(),tags:{metric:e,rating:i,...r}};this.client.captureEvent(o)}destroy(){}},F=N;var q=class{constructor(){this.name="network";this.version="1.0.0";this.config={captureSuccess:!1,captureBody:!1,captureRequestBody:!1,captureResponseBody:!1,captureRequestHeaders:["content-type","user-agent"],captureResponseHeaders:["content-type","content-length"],errorSampleRate:1,successSampleRate:.01,slowThreshold:3e3,slowSampleRate:1,ignoreStatusCodes:[]}}setup(e){this.client=e,this.loadConfig(),this.patchFetch(),this.patchXHR()}loadConfig(){let e=this.client.config;e&&v(e.network)&&(this.config={...this.config,...e.network}),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnore(e){return w(e,this.config.ignoreUrls||[])}shouldSample(e,t){var r,i,o,a;let n=e?(r=this.config.errorSampleRate)!=null?r:1:(i=this.config.successSampleRate)!=null?i:.01;return!e&&t>=((o=this.config.slowThreshold)!=null?o:3e3)&&(n=(a=this.config.slowSampleRate)!=null?a:1),n>=1?!0:n<=0?!1:Math.random(){var y;let d=p()-r,f=u.ok,h=u.status;return(y=e.config.ignoreStatusCodes)!=null&&y.includes(h)||!e.shouldSample(!f,d)||(!f||e.config.captureSuccess)&&e.reportNetwork({sub_type:"fetch",method:o.toUpperCase(),url:a,status_code:h,duration:d,success:f,error:f?void 0:`HTTP ${h}`}),u},u=>{let d=p()-r;throw e.shouldSample(!0,d)&&e.reportNetwork({sub_type:"fetch",method:o.toUpperCase(),url:a,duration:d,success:!1,error:(u==null?void 0:u.message)||"Network error"}),u}),c}}patchXHR(){if(typeof XMLHttpRequest=="undefined")return;this.originalXHROpen=XMLHttpRequest.prototype.open,this.originalXHRSend=XMLHttpRequest.prototype.send;let e=this;XMLHttpRequest.prototype.open=function(t,n){return this._lightMethod=t,this._lightUrl=typeof n=="string"?n:n.toString(),e.originalXHROpen.apply(this,arguments)},XMLHttpRequest.prototype.send=function(t){let n=p(),r=this._lightMethod||"GET",i=this._lightUrl||"";if(e.shouldIgnore(i))return e.originalXHRSend.apply(this,arguments);let o=M(i),a=0;t&&typeof t=="string"&&(a=t.length);let c=()=>{var y;let u=p()-n,d=this.status,f=d>=200&&d<300;if((y=e.config.ignoreStatusCodes)!=null&&y.includes(d)){this.removeEventListener("loadend",c);return}if(!e.shouldSample(!f,u)){this.removeEventListener("loadend",c);return}if(f&&!e.config.captureSuccess){this.removeEventListener("loadend",c);return}let h=0;try{let P=this.getResponseHeader("content-length");P&&(h=parseInt(P,10)),!h&&this.responseText&&(h=this.responseText.length)}catch(P){}e.reportNetwork({sub_type:"xhr",method:r.toUpperCase(),url:o,status_code:d,duration:u,request_size:a||void 0,response_size:h||void 0,success:f,error:f?void 0:`HTTP ${d}`}),this.removeEventListener("loadend",c)};return this.addEventListener("loadend",c),e.originalXHRSend.apply(this,arguments)}}reportNetwork(e){let t={type:"network",level:e.success?"info":"error",timestamp:p(),tags:{method:e.method,sub_type:e.sub_type,success:String(e.success)},...e};this.client.captureEvent(t)}destroy(){this.originalFetch&&(window.fetch=this.originalFetch),this.originalXHROpen&&(XMLHttpRequest.prototype.open=this.originalXHROpen),this.originalXHRSend&&(XMLHttpRequest.prototype.send=this.originalXHRSend)}},U=q;var H=class{constructor(){this.name="behavior";this.version="1.0.0";this.config={capturePV:!0,captureClick:!0,captureRoute:!0,captureDuration:!0,captureScroll:!0,clickThrottle:300,scrollThrottle:1e3,sampleRate:.1};this.lastClickTime=0;this.lastScrollTime=0;this.maxScrollDepth=0;this.pageEnterTime=0;this.scrollReported=!1}setup(e){this.client=e,this.loadConfig(),this.shouldSample()&&(this.config.capturePV&&this.trackPV(),this.config.captureClick&&this.trackClick(),this.config.captureRoute&&this.trackRoute(),this.config.captureDuration&&this.trackPageDuration(),this.config.captureScroll&&this.trackScroll())}loadConfig(){let e=this.client.config;e&&v(e.behavior)&&(this.config={...this.config,...e.behavior})}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:.1;return e>=1?!0:e<=0?!1:Math.random(){var a,c,u;let t=p(),n=(a=this.config.clickThrottle)!=null?a:300;if(t-this.lastClickTime{let i=g();if(i!==e){let o=e,a=i;e=i,this.reportBehavior({sub_type:"route",page_url:a,referrer:o,properties:{from:o,to:a}}),this.pageEnterTime=p(),this.maxScrollDepth=0,this.scrollReported=!1}},n=history.pushState,r=history.replaceState;history.pushState=function(){let i=n.apply(this,arguments);return setTimeout(t,0),i},history.replaceState=function(){let i=r.apply(this,arguments);return setTimeout(t,0),i},window.addEventListener("popstate",()=>{setTimeout(t,0)}),window.addEventListener("hashchange",()=>{setTimeout(t,0)})}trackPageDuration(){if(typeof window=="undefined")return;this.pageEnterTime=p();let e=()=>{let t=p()-this.pageEnterTime;t>1e3&&(this.config.captureScroll&&!this.scrollReported&&this.reportScroll(),this.reportBehavior({sub_type:"duration",page_url:g(),properties:{duration:t,max_scroll_depth:this.maxScrollDepth}}))};document.addEventListener("visibilitychange",()=>{document.hidden?e():this.pageEnterTime=p()}),window.addEventListener("beforeunload",e),window.addEventListener("pagehide",e)}trackScroll(){if(typeof window=="undefined")return;let e=()=>{var i;let t=p(),n=(i=this.config.scrollThrottle)!=null?i:1e3;if(t-this.lastScrollTimethis.maxScrollDepth&&(this.maxScrollDepth=r)};window.addEventListener("scroll",e,{passive:!0})}reportScroll(){this.scrollReported||(this.scrollReported=!0,this.reportBehavior({sub_type:"scroll",page_url:g(),properties:{max_depth:this.maxScrollDepth}}))}reportBehavior(e){let t={type:"behavior",level:"info",timestamp:p(),tags:{sub_type:e.sub_type,page_url:g()},...e};this.client.captureEvent(t)}destroy(){}},$=H;var Se="light-sentry-offline",m="events",W=1e3,j=class{constructor(){this.name="offline";this.version="1.0.0";this.config={maxEvents:W};this.db=null;this.isOnline=!0;this.isSyncing=!1;this.pendingEvents=[]}setup(e){this.client=e,this.loadConfig(),this.initDatabase(),this.setupNetworkListeners()}loadConfig(){let e=this.client.config;e&&typeof e.offline=="object"&&(this.config={...this.config,...e.offline})}async initDatabase(){if(typeof indexedDB=="undefined"){console.warn("[LightSDK] OfflinePlugin: IndexedDB not available");return}return new Promise((e,t)=>{let n=indexedDB.open(Se,1);n.onerror=()=>{console.warn("[LightSDK] OfflinePlugin: Failed to open IndexedDB"),t(n.error)},n.onsuccess=()=>{this.db=n.result,e()},n.onupgradeneeded=r=>{let i=r.target.result;i.objectStoreNames.contains(m)||i.createObjectStore(m,{keyPath:"id"}).createIndex("timestamp","timestamp",{unique:!1})}})}setupNetworkListeners(){typeof window!="undefined"&&(this.isOnline=navigator.onLine,window.addEventListener("online",()=>{console.info("[LightSDK] OfflinePlugin: Network online"),this.isOnline=!0,this.syncPendingEvents()}),window.addEventListener("offline",()=>{console.info("[LightSDK] OfflinePlugin: Network offline"),this.isOnline=!1}))}generateId(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}async saveToIndexedDB(e){if(this.db)return new Promise((t,n)=>{let i=this.db.transaction([m],"readwrite").objectStore(m),o=i.count();o.onsuccess=async()=>{let a=o.result;a>=(this.config.maxEvents||W)&&await this.deleteOldestEvents(a-(this.config.maxEvents||W)+1);let c={id:this.generateId(),event:{...e,offline:!0},timestamp:p()},u=i.add(c);u.onsuccess=()=>t(),u.onerror=()=>n(u.error)}})}async deleteOldestEvents(e){if(!(!this.db||e<=0))return new Promise((t,n)=>{let a=this.db.transaction([m],"readwrite").objectStore(m).index("timestamp").openCursor(),c=0;a.onsuccess=u=>{let d=u.target.result;d&&cn(a.error)})}async getAllStoredEvents(){return this.db?new Promise((e,t)=>{let o=this.db.transaction([m],"readonly").objectStore(m).index("timestamp").getAll();o.onsuccess=()=>{let a=o.result||[];a.sort((c,u)=>c.timestamp-u.timestamp),e(a)},o.onerror=()=>t(o.error)}):[]}async clearStoredEvents(){if(this.db)return new Promise((e,t)=>{let i=this.db.transaction([m],"readwrite").objectStore(m).clear();i.onsuccess=()=>e(),i.onerror=()=>t(i.error)})}async syncPendingEvents(){if(!(!this.isOnline||this.isSyncing)){this.isSyncing=!0,console.info("[LightSDK] OfflinePlugin: Syncing pending events");try{let e=await this.getAllStoredEvents();if(e.length===0){console.info("[LightSDK] OfflinePlugin: No pending events to sync"),this.isSyncing=!1;return}let t=50;for(let n=0;no.event);try{await(async()=>new Promise((a,c)=>{this.client.emit("sync:offline",i),a()}))(),console.info(`[LightSDK] OfflinePlugin: Synced ${r.length} events`)}catch(o){console.error("[LightSDK] OfflinePlugin: Failed to sync batch",o)}}await this.clearStoredEvents(),console.info("[LightSDK] OfflinePlugin: All pending events synced")}catch(e){console.error("[LightSDK] OfflinePlugin: Sync failed",e)}finally{this.isSyncing=!1}}}beforeReport(e){if(!this.isOnline)return this.saveToIndexedDB(e).catch(n=>{console.error("[LightSDK] OfflinePlugin: Failed to save event offline",n)}),null;let t={...e};return delete t.offline,t}destroy(){this.db&&(this.db.close(),this.db=null)}},K=j;var l=null;function J(s){var r;let e=new B(s),t=[new O,new K],n=((r=s.plugins)==null?void 0:r.filter(i=>typeof i=="string"))||[];(n.includes("performance")||n.includes("all"))&&t.push(new F),(n.includes("network")||n.includes("all"))&&t.push(new U),(n.includes("behavior")||n.includes("all"))&&t.push(new $);for(let i of t)e.use(i);if(s.plugins)for(let i of s.plugins)typeof i=="object"&&i!==null&&"name"in i&&"setup"in i&&e.use(i);return e.init(),l=e,e}function Z(){return l}function Y(s){l==null||l.captureException(s)}function ee(s,e){l==null||l.captureMessage(s,e)}function te(s){l==null||l.captureEvent(s)}function ne(s){l==null||l.setUser(s)}function re(s,e){l==null||l.setTag(s,e)}function ie(s){l==null||l.setTags(s)}function se(s,e){l==null||l.setExtra(s,e)}function oe(s){l==null||l.addBreadcrumb(s)}async function ae(){await(l==null?void 0:l.flush())}function ce(){l==null||l.disable()}function ue(){l==null||l.enable()}var xe={init:J,getClient:Z,captureException:Y,captureMessage:ee,captureEvent:te,setUser:ne,setTag:re,setTags:ie,setExtra:se,addBreadcrumb:oe,flush:ae,disable:ce,enable:ue};
+"use strict";var D=Object.defineProperty;var we=Object.getOwnPropertyDescriptor;var Ee=Object.getOwnPropertyNames;var Se=Object.prototype.hasOwnProperty;var xe=(o,e)=>{for(var t in e)D(o,t,{get:e[t],enumerable:!0})},Re=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Ee(e))!Se.call(o,r)&&r!==t&&D(o,r,{get:()=>e[r],enumerable:!(n=we(e,r))||n.enumerable});return o};var ke=o=>Re(D({},"__esModule",{value:!0}),o);var De={};xe(De,{BehaviorPlugin:()=>V,Client:()=>q,ErrorPlugin:()=>j,NetworkPlugin:()=>A,OfflinePlugin:()=>G,PerformancePlugin:()=>W,SamplingPlugin:()=>Z,addBreadcrumb:()=>me,captureEvent:()=>de,captureException:()=>ue,captureMessage:()=>le,default:()=>Oe,disable:()=>ye,enable:()=>be,flush:()=>ve,getClient:()=>ce,init:()=>ae,setExtra:()=>ge,setTag:()=>he,setTags:()=>fe,setUser:()=>pe});module.exports=ke(De);function ee(o){try{let e=o.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);if(!e)throw new Error("Invalid DSN format");let[,t,n,r,i]=e;return{protocol:t,publicKey:n,host:r,projectId:i}}catch(e){throw new Error(`Failed to parse DSN: ${e.message}`)}}function k(o){return`${o.protocol}://${o.host}/api/${o.projectId}/envelope/?sentry_key=${o.publicKey}`}function te(){return"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".replace(/[x]/g,()=>(Math.random()*16|0).toString(16))}function d(){return Date.now()}function y(o){return o!==null&&typeof o=="object"&&!Array.isArray(o)}function T(o,e){return!e||e.length===0?!1:e.some(t=>typeof t=="string"?o.includes(t):t.test(o))}function Te(o){let e=5381,t=o.length;for(;t;)e=e*33^o.charCodeAt(--t);return e>>>0}function F(o){return Te(o).toString(16)}function L(o,e,t=[]){let n=t.slice(0,3).map(i=>`${i.filename}:${i.lineno||0}`).join("|"),r=e.replace(/['"]?\d+['"]?/g,"").replace(/\s+/g," ").trim();return F(`${o}:${r}:${n}`)}function P(o,e={}){if(!o)return[];let{captureNodeModules:t=!1,captureColumn:n=!0,relativePathOnly:r=!1,maxFrames:i}=e,s=[],a=o.split(`
+`),c=typeof location!="undefined"?`${location.protocol}//${location.host}`:"";for(let p of a){let w=p.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(w){let[,f,h,v,g]=w,E=h.includes("node_modules");if(!t&&E)continue;let S=h;r&&c&&h.startsWith(c)&&(S=h.substring(c.length)),s.push({filename:S,function:f,lineno:parseInt(v,10),colno:n?parseInt(g,10):void 0,in_app:!E})}else{let f=p.match(/^\s*at\s*(.+?)@(.+?):(\d+):(\d+)\s*$/);if(f){let[,v,g,E,S]=f,R=g.includes("node_modules");if(!t&&R)continue;let Y=g;r&&c&&g.startsWith(c)&&(Y=g.substring(c.length)),s.push({filename:Y,function:v||"",lineno:parseInt(E,10),colno:n?parseInt(S,10):void 0,in_app:!R});continue}let h=p.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(h){let[,v,g,E]=h,S=v.includes("node_modules");if(!t&&S)continue;let R=v;r&&c&&v.startsWith(c)&&(R=v.substring(c.length)),s.push({filename:R,lineno:parseInt(g,10),colno:n?parseInt(E,10):void 0,in_app:!S})}}}let u=s.reverse();return i!==void 0&&i>0?u.slice(0,i):u}function Le(o){let e=[{name:"WeChat",pattern:/MicroMessenger\/([\d.]+)/i},{name:"QQ Browser",pattern:/QIBrowser\/([\d.]+)/i},{name:"UC Browser",pattern:/UCBrowser\/([\d.]+)/i},{name:"Edge",pattern:/Edg\/([\d.]+)/i},{name:"Edge",pattern:/Edge\/([\d.]+)/i},{name:"Opera",pattern:/OPR\/([\d.]+)/i},{name:"Opera",pattern:/Opera\/([\d.]+)/i},{name:"Firefox",pattern:/Firefox\/([\d.]+)/i},{name:"Safari",pattern:/Version\/([\d.]+).*Safari/i},{name:"Chrome",pattern:/Chrome\/([\d.]+)/i},{name:"IE",pattern:/MSIE ([\d.]+)/i},{name:"IE",pattern:/Trident.*rv:([\d.]+)/i}];for(let t of e){let n=o.match(t.pattern);if(n){let r=n[1]||"",i=r.split(".")[0]||"";return{name:t.name,version:r,major:i}}}return{name:"Unknown",version:"",major:""}}function Pe(o){let e=[{name:"Windows 11",pattern:/Windows NT 10\.0.*Win64.*x64/i},{name:"Windows 10",pattern:/Windows NT 10\.0/i},{name:"Windows 8.1",pattern:/Windows NT 6\.3/i},{name:"Windows 8",pattern:/Windows NT 6\.2/i},{name:"Windows 7",pattern:/Windows NT 6\.1/i},{name:"Windows Vista",pattern:/Windows NT 6\.0/i},{name:"Windows XP",pattern:/Windows NT 5\.1/i},{name:"macOS",pattern:/Mac OS X ([\d_.]+)/i},{name:"iOS",pattern:/OS ([\d_.]+) like Mac OS X/i},{name:"Android",pattern:/Android ([\d.]+)/i},{name:"Android",pattern:/Android/i},{name:"Linux",pattern:/Linux/i}];for(let t of e){let n=o.match(t.pattern);if(n){let r=n[1]||"";return r&&(r=r.replace(/_/g,".")),{name:t.name,version:r}}}return{name:"Unknown",version:""}}function Ce(o){let e=/Mobile|Android|iPhone|iPod|BlackBerry|Windows Phone|Opera Mini/i.test(o),t=/Tablet|iPad|PlayBook|Kindle|Silk/i.test(o),n="desktop";t?n="tablet":e&&(n="mobile");let r="",i="",s=o.match(/iPhone/i),a=o.match(/iPad/i),c=o.match(/SM-([A-Z0-9]+)/i),u=o.match(/Pixel ([0-9A-Z]+)/i),p=o.match(/HUAWEI ([A-Z0-9]+)/i);return s?(r="Apple",i="iPhone"):a?(r="Apple",i="iPad"):c?(r="Samsung",i=c[1]):u?(r="Google",i=`Pixel ${u[1]}`):p&&(r="Huawei",i=p[1]),{type:n,vendor:r,model:i}}function ne(){let o=typeof navigator!="undefined"?navigator.userAgent:"",e=Le(o),t=Pe(o),n=Ce(o);return{browser:e,os:t,device:n,url:typeof location!="undefined"?location.href:"",referrer:typeof document!="undefined"?document.referrer:"",title:typeof document!="undefined"?document.title:"",language:typeof navigator!="undefined"?navigator.language:"",user_agent:o,screen_width:typeof screen!="undefined"?screen.width:0,screen_height:typeof screen!="undefined"?screen.height:0,viewport_width:typeof window!="undefined"?window.innerWidth:0,viewport_height:typeof window!="undefined"?window.innerHeight:0}}function b(){return typeof location!="undefined"?location.href:""}function re(){return typeof document!="undefined"?document.referrer:""}function x(o){if(!o)return o;try{let e=new URL(o),t=["password","passwd","pwd","token","access_token","refresh_token","api_key","apikey","key","secret","private_key","code","auth","id_token","idToken","session","session_id"],n=e.searchParams,r=!1;for(let i of Array.from(n.keys())){let s=i.toLowerCase().replace(/[-_]/g,"_");t.some(a=>s.includes(a))&&(n.set(i,"[Filtered]"),r=!0)}if(e.hash&&e.hash.length>1){let i=e.hash.substring(1);if(i.includes("=")){let s=new URLSearchParams(i),a=!1;for(let c of Array.from(s.keys())){let u=c.toLowerCase().replace(/[-_]/g,"_");t.some(p=>u.includes(p))&&(s.set(c,"[Filtered]"),a=!0)}a&&(e.hash="#"+s.toString(),r=!0)}}return r?e.toString():o}catch(e){return o}}function C(o,e=[]){return!e||e.length===0?!1:e.some(t=>{if(typeof t=="string"){if(t.includes("*")){let n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(n).test(o)}return o.includes(t)}return t.test(o)})}var Me={Chrome:"c",Safari:"s",Firefox:"f",Edge:"e",Opera:"o",IE:"i",WeChat:"w","QQ Browser":"q","UC Browser":"u","Mobile Chrome":"cm","Mobile Safari":"sm"},Be={"Windows 11":"w11","Windows 10":"w10","Windows 8.1":"w81","Windows 8":"w8","Windows 7":"w7","Windows Vista":"wv","Windows XP":"wxp",macOS:"m",iOS:"i",Android:"a",Linux:"l"};function U(){let o=ne(),e=Me[o.browser.name]||o.browser.name.toLowerCase(),t=Be[o.os.name]||o.os.name.toLowerCase();return{browser:{name:e,version:o.browser.major},os:{name:t,version:o.os.version.split(".")[0]},device:{type:o.device.type,brand:o.device.vendor||void 0,model:o.device.model||void 0}}}function ie(){let o=ne(),e=`${o.browser.name}|${o.browser.major}|${o.os.name}|${o.os.version.split(".")[0]}|${o.device.type}|${o.device.vendor}|${o.device.model}|${o.language}`;return F(e).substring(0,16)}var Ie={enabled:!0,sampleRate:1,maxQueueSize:100,flushInterval:5e3,maxRetries:3,retryDelay:1e3,environment:"production",contextLevel:{fatal:{maxStackFrames:5,maxBreadcrumbs:20},error:{maxStackFrames:5,maxBreadcrumbs:10},warning:{maxStackFrames:3,maxBreadcrumbs:5},info:{maxStackFrames:0,maxBreadcrumbs:0},debug:{maxStackFrames:0,maxBreadcrumbs:0}}},M=class{constructor(e){this.contexts=null;this.contextId=null;this.contextReported=!1;this.config={...Ie,...e}}markContextReported(){this.contextReported=!0}get(e){return this.config[e]}getAll(){return{...this.config}}set(e,t){this.config[e]=t}setUser(e){this.config.user=e||void 0}setTags(e){this.config.tags={...this.config.tags||{},...e}}setTag(e,t){this.config.tags||(this.config.tags={}),this.config.tags[e]=t}setExtra(e,t){this.config.extra||(this.config.extra={}),this.config.extra[e]=t}isIgnoredError(e){return T(e,this.config.ignoreErrors||[])}getEncodedContext(){return this.contexts||(this.contexts=U()),this.contexts}applyToEvent(e,t=!0){var i,s,a,c;let n={...e};if(e.tags&&(n.tags={...e.tags}),e.request&&(n.request={...e.request}),"exception"in e&&e.exception){let u=e.exception;n.exception={...u,stacktrace:u.stacktrace?{...u.stacktrace,frames:u.stacktrace.frames?u.stacktrace.frames.map(p=>({...p})):void 0}:void 0}}e.breadcrumbs&&(n.breadcrumbs=e.breadcrumbs.map(u=>({...u}))),e.user&&(n.user={...e.user}),e.contexts&&(n.contexts={...e.contexts}),this.config.release&&(n.release=this.config.release),this.config.environment&&(n.environment=this.config.environment),this.config.user&&(n.user=this.config.user),this.config.tags&&(n.tags={...this.config.tags,...e.tags||{}}),n.request?n.request.url||(n.request.url=b()):n.request={url:b(),referrer:re()},this.contextId||(this.contextId=ie()),n.context_id=this.contextId;let r=this.applyContextLevel(n);return t&&!this.contextReported&&(this.contexts||(this.contexts=U()),r.contexts=this.contexts,r.env={b:(i=this.contexts.browser)==null?void 0:i.name,bv:(s=this.contexts.browser)==null?void 0:s.version,os:(a=this.contexts.os)==null?void 0:a.name,osv:(c=this.contexts.os)==null?void 0:c.version}),r}applyContextLevel(e){let t=this.config.contextLevel;if(!t)return e;let n=e.level||"error",r=t[n]||t.error,i={...e};if("exception"in e&&e.exception&&typeof e.exception=="object"){let s=e.exception;if(s.stacktrace&&typeof s.stacktrace=="object"){let a=s.stacktrace;Array.isArray(a.frames)&&(r.maxStackFrames===0?delete i.exception:i.exception={...s,stacktrace:{...a,frames:a.frames.slice(0,r.maxStackFrames)}})}}return e.breadcrumbs&&Array.isArray(e.breadcrumbs)&&(r.maxBreadcrumbs===0?delete i.breadcrumbs:i.breadcrumbs=e.breadcrumbs.slice(-r.maxBreadcrumbs)),i}};var B=class{constructor(){this.handlers=new Map}on(e,t){this.handlers.has(e)||this.handlers.set(e,[]),this.handlers.get(e).push(t)}off(e,t){let n=this.handlers.get(e);if(n){let r=n.indexOf(t);r>-1&&n.splice(r,1)}}emit(e,...t){let n=this.handlers.get(e);if(n)for(let r of n)try{r(...t)}catch(i){console.error("[LightSDK] EventBus handler error:",i)}}once(e,t){let n=(...r)=>{this.off(e,n),t(...r)};this.on(e,n)}destroy(){this.handlers.clear()}};var I=class{constructor(e,t,n,r,i){this.queue=[];this.timer=null;this.lastFlushTime=0;this.isFlushing=!1;this.dedupeMap=new Map;this.pendingCounts=new Map;this.errorRateWindow={};this.paused=!1;this.pauseTimer=null;this.maxSize=e,this.flushInterval=t,this.eventBus=n,this.flushCallback=r,this.syncFlushCallback=i}start(){this.startTimer(),this.setupVisibilityListener()}startTimer(){this.timer||(this.timer=setInterval(()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flush()},this.flushInterval))}setupVisibilityListener(){this.onVisibilityChange=()=>{document.hidden&&(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},typeof document!="undefined"&&document.addEventListener("visibilitychange",this.onVisibilityChange),this.onBeforeUnload=()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},this.onPagehide=()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},typeof window!="undefined"&&(window.addEventListener("beforeunload",this.onBeforeUnload),window.addEventListener("pagehide",this.onPagehide))}checkInfiniteLoop(e){let t=d(),n=t-1e3;this.errorRateWindow[e]||(this.errorRateWindow[e]=[]);let r=this.errorRateWindow[e];for(r.push(t);r.length>0&&r[0]10?(this.paused||(console.warn("[LightSDK] Infinite loop detected, pausing SDK for 60s",{fingerprint:e,count:r.length}),this.eventBus.emit("error",new Error("Infinite loop detected")),this.paused=!0,this.pauseTimer&&clearTimeout(this.pauseTimer),this.pauseTimer=setTimeout(()=>{this.paused=!1,this.errorRateWindow={},console.info("[LightSDK] Resumed after infinite loop detection")},6e4)),!0):!1}enqueue(e){if(this.paused)return;this.cleanupExpiredDedupeEntries();let t=this.getDedupeKey(e);if(t){if(this.checkInfiniteLoop(t))return;let n=d(),r=this.dedupeMap.get(t);if(r)if(n-r.lastTime<6e4){if(r.count>=3){if(!this.pendingCounts.has(t))this.pendingCounts.set(t,{count:1,ts_start:n,ts_end:n});else{let i=this.pendingCounts.get(t);i.count++,i.ts_end=n}return}r.count++,r.lastTime=n}else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!0}),this.pendingCounts.delete(t);else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!1})}this.queue.push(e),this.eventBus.emit("event",e),this.queue.length>=this.maxSize&&this.flush()}cleanupExpiredDedupeEntries(){if(this.dedupeMap.size<100&&Math.random()>.1)return;let e=d(),t=6e4;for(let[n,r]of this.dedupeMap)e-r.lastTime>t&&this.dedupeMap.delete(n)}buildCountEvents(){let e=[];for(let[t,n]of this.pendingCounts.entries()){let r={type:"count",fingerprint:t,count:n.count,ts_start:n.ts_start,ts_end:n.ts_end,timestamp:d(),level:"info"};e.push(r)}return e}getDedupeKey(e){if(e.type==="error"){let t=e;return t.fingerprint||t.message}return null}async flush(){if(this.queue.length===0&&this.pendingCounts.size===0||this.isFlushing)return;this.isFlushing=!0;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=d(),this.eventBus.emit("report",n);try{await this.flushCallback(n),this.pendingCounts.clear(),this.eventBus.emit("reported",n)}catch(r){throw e.forEach(i=>this.queue.unshift(i)),r}finally{this.isFlushing=!1}}flushSync(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=new Map(this.pendingCounts),n=this.buildCountEvents(),r=[...e,...n];this.lastFlushTime=d(),this.eventBus.emit("report",r);try{this.syncFlushCallback(r),this.pendingCounts.clear(),this.eventBus.emit("reported",r)}catch(i){console.error("[LightSDK] Sync flush failed",i),e.forEach(s=>this.queue.unshift(s));for(let[s,a]of t)this.pendingCounts.has(s)||this.pendingCounts.set(s,a)}}size(){return this.queue.length}destroy(){this.timer&&(clearInterval(this.timer),this.timer=null),this.pauseTimer&&(clearTimeout(this.pauseTimer),this.pauseTimer=null),typeof document!="undefined"&&this.onVisibilityChange&&document.removeEventListener("visibilitychange",this.onVisibilityChange),typeof window!="undefined"&&(this.onBeforeUnload&&window.removeEventListener("beforeunload",this.onBeforeUnload),this.onPagehide&&window.removeEventListener("pagehide",this.onPagehide)),this.dedupeMap.clear(),this.pendingCounts.clear(),this.errorRateWindow={}}};var se={event_id:"eid",timestamp:"ts",start_timestamp:"sts",type:"t",level:"l",message:"m",platform:"p",release:"r",environment:"e",fingerprint:"fp",context_id:"cid",user:"u",tags:"tg",exception:"ex",request:"req",contexts:"ctx",breadcrumbs:"bc",transaction:"tx",duration:"d",spans:"sp",extra:"xt"},Qe=Object.fromEntries(Object.entries(se).map(([o,e])=>[e,o]));function oe(o){if(!o||typeof o!="object")return o;let e={};for(let[t,n]of Object.entries(o)){let r=se[t]||t;e[r]=n}return e}var _=class{constructor(e,t,n){this.useShortFields=!0;this.dsn=e,this.maxRetries=t,this.retryDelay=n}async report(e){if(e.length===0)return;let t=this.buildEnvelope(e),n=0;for(;n<=this.maxRetries;)try{await this.send(t,!1);return}catch(r){let i=r.status;if(i&&i>=400&&i<500||(n++,n>this.maxRetries))throw r;let s=Math.min(this.retryDelay*Math.pow(2,n-1),5e3);await this.delay(s)}}reportSync(e){if(e.length===0)return;let t=this.buildEnvelope(e);try{if(this.sendSync(t))return}catch(n){}this.sendViaImage(t)}buildEnvelope(e){let t=this.extractSharedMeta(e),n={event_id:te(),sent_at:new Date().toISOString(),meta:t};this.useShortFields&&(n._sf=1);let r=this.applyRelativeTimestamps(e,n),s=[JSON.stringify(n)];for(let a of r){let c=this.stripSharedFields(a,t);this.useShortFields&&(c=oe(c));let u=JSON.stringify(c),p=JSON.stringify({type:this.getEnvelopeType(a),length:u.length});s.push(p,u)}return s.join(`
+`)}applyRelativeTimestamps(e,t){if(e.length<=1)return e;let n=[],r=e[0].timestamp?new Date(e[0].timestamp).getTime():Date.now();t._rt=1,t._bt=r;for(let i=0;is.release===t.release)&&(n.release=t.release),t.environment&&e.every(s=>s.environment===t.environment)&&(n.environment=t.environment),t.user&&e.every(s=>JSON.stringify(s.user)===JSON.stringify(t.user))&&(n.user=t.user);let r=t.env;return r&&(n.env=r),n}stripSharedFields(e,t){let n={...e};return t.release&&n.release===t.release&&delete n.release,t.environment&&n.environment===t.environment&&delete n.environment,t.user&&JSON.stringify(n.user)===JSON.stringify(t.user)&&delete n.user,t.env&&delete n.env,n}getEnvelopeType(e){switch(e.type){case"error":return"event";case"performance":return"transaction";default:return e.type}}async send(e,t=!1){let n=k(this.dsn);if(navigator.sendBeacon)try{let r=new Blob([e],{type:"application/x-sentry-envelope"});if(navigator.sendBeacon(n,r))return}catch(r){}if(typeof fetch=="function")try{let r=new AbortController,i=setTimeout(()=>r.abort(),1e4),s=await fetch(n,{method:"POST",body:e,headers:{"Content-Type":"application/x-sentry-envelope"},keepalive:!0,signal:r.signal});if(clearTimeout(i),s.ok)return;let a=new Error(`HTTP ${s.status}`);throw a.status=s.status,a}catch(r){throw r}if(typeof XMLHttpRequest!="undefined")return new Promise((r,i)=>{let s=new XMLHttpRequest;s.open("POST",n,!t),s.setRequestHeader("Content-Type","application/x-sentry-envelope"),s.timeout=1e4,s.onload=()=>{if(s.status>=200&&s.status<300)r();else{let a=new Error(`HTTP ${s.status}`);a.status=s.status,i(a)}},s.onerror=()=>i(new Error("Network error")),s.ontimeout=()=>i(new Error("Request timeout")),s.send(e)});throw new Error("No transport available")}sendSync(e){let t=k(this.dsn);if(navigator.sendBeacon)try{let n=new Blob([e],{type:"application/x-sentry-envelope"});return navigator.sendBeacon(t,n)}catch(n){return!1}if(typeof XMLHttpRequest!="undefined")try{let n=new XMLHttpRequest;return n.open("POST",t,!1),n.setRequestHeader("Content-Type","application/x-sentry-envelope"),n.send(e),n.status>=200&&n.status<300}catch(n){return!1}return!1}sendViaImage(e){try{let t=k(this.dsn),n=new Image,r=encodeURIComponent(btoa(e)),i=t.includes("?")?"&":"?";return n.src=t+i+"sentry_data="+r.substring(0,2e3),!0}catch(t){return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};var O=class{constructor(e){this.plugins=new Map;this.client=e}add(e){if(!this.plugins.has(e.name)){this.plugins.set(e.name,e);try{e.setup(this.client)}catch(t){console.error(`[LightSDK] Plugin "${e.name}" setup error:`,t)}}}remove(e){let t=this.plugins.get(e);if(t){if(t.destroy)try{t.destroy()}catch(n){console.error(`[LightSDK] Plugin "${e}" destroy error:`,n)}this.plugins.delete(e)}}get(e){return this.plugins.get(e)}has(e){return this.plugins.has(e)}applyBeforeReport(e){let t=e;for(let n of this.plugins.values())if(n.beforeReport)try{let r=n.beforeReport(t);if(r===null)return null;t=r}catch(r){console.error(`[LightSDK] Plugin "${n.name}" beforeReport error:`,r)}return t}applyAfterReport(e){for(let t of this.plugins.values())if(t.afterReport)try{t.afterReport(e)}catch(n){console.error(`[LightSDK] Plugin "${t.name}" afterReport error:`,n)}}destroy(){for(let e of this.plugins.values())if(e.destroy)try{e.destroy()}catch(t){console.error(`[LightSDK] Plugin "${e.name}" destroy error:`,t)}this.plugins.clear()}};var N=class{constructor(e){this.breadcrumbs=[];this.enabled=!0;var t,n,r,i;if(!e.dsn)throw new Error("DSN is required");this.dsn=ee(e.dsn),this.configManager=new M(e),this.eventBus=new B,this.reporter=new _(this.dsn,(t=this.configManager.get("maxRetries"))!=null?t:3,(n=this.configManager.get("retryDelay"))!=null?n:1e3),this.pluginManager=new O(this),this.queue=new I((r=this.configManager.get("maxQueueSize"))!=null?r:100,(i=this.configManager.get("flushInterval"))!=null?i:5e3,this.eventBus,async s=>this.flushEvents(s),s=>this.syncFlushEvents(s)),this.enabled=this.configManager.get("enabled")!==!1}get maxBreadcrumbs(){var e;return(e=this.configManager.get("maxBreadcrumbs"))!=null?e:20}get config(){return this.configManager.getAll()}init(){this.enabled&&(this.queue.start(),this.emit("ready"))}async flushEvents(e){let t=[];for(let i of e){let s=this.pluginManager.applyBeforeReport(i);s&&t.push(s)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let s=this.configManager.applyToEvent(i,n);n=!1;let a=this.configManager.get("beforeSend"),c=a?a(s):s;c&&r.push(c)}if(r.length!==0){await this.reporter.report(r),this.configManager.markContextReported();for(let i of r)this.pluginManager.applyAfterReport(i)}}syncFlushEvents(e){let t=[];for(let i of e){let s=this.pluginManager.applyBeforeReport(i);s&&t.push(s)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let s=this.configManager.applyToEvent(i,n);n=!1;let a=this.configManager.get("beforeSend"),c=a?a(s):s;c&&r.push(c)}r.length!==0&&(this.reporter.reportSync(r),this.configManager.markContextReported())}on(e,t){this.eventBus.on(e,t)}off(e,t){this.eventBus.off(e,t)}emit(e,...t){this.eventBus.emit(e,...t)}captureException(e){if(!this.enabled)return;let t=this.buildErrorEvent(e);this.configManager.isIgnoredError(t.message)||(t.breadcrumbs=[...this.breadcrumbs],this.queue.enqueue(t))}captureMessage(e,t="info"){if(!this.enabled)return;let n={type:"error",level:t,message:e,timestamp:d(),breadcrumbs:[...this.breadcrumbs]};this.configManager.isIgnoredError(e)||this.queue.enqueue(n)}captureEvent(e){if(!this.enabled)return;let t={timestamp:d(),level:"info",breadcrumbs:[...this.breadcrumbs],...e};this.queue.enqueue(t)}buildErrorEvent(e){let t=d();if(e instanceof Error){let r=P(e.stack,{maxFrames:5}),i=L(e.name,e.message,r);return{type:"error",level:"error",message:e.message,timestamp:t,exception:{type:e.name,value:e.message,stacktrace:{frames:r}},fingerprint:i}}return{type:"error",level:"error",message:String(e),timestamp:t}}setUser(e){this.configManager.setUser(e)}setTag(e,t){this.configManager.setTag(e,t)}setTags(e){this.configManager.setTags(e)}setExtra(e,t){this.configManager.setExtra(e,t)}addBreadcrumb(e){let t={...e,timestamp:d()};this.breadcrumbs.push(t),this.breadcrumbs.length>this.maxBreadcrumbs&&this.breadcrumbs.shift()}async flush(){await this.queue.flush()}disable(){this.enabled=!1}enable(){this.enabled=!0}use(e){this.pluginManager.add(e)}destroy(){this.queue.destroy(),this.pluginManager.destroy(),this.eventBus.destroy(),this.breadcrumbs=[]}},q=N;var H=class{constructor(){this.name="error";this.version="1.0.0";this.config={maxStackFrames:5,captureNodeModules:!1,captureColumn:!0,relativePathOnly:!1,resourceSampleRate:.1}}setup(e){this.client=e,this.loadConfig(),this.setupGlobalError(),this.setupUnhandledRejection(),this.setupResourceError()}loadConfig(){let e=this.client.config;e&&y(e.error)&&(this.config={...this.config,...e.error}),e.ignoreErrors&&(this.config.ignoreErrors=[...this.config.ignoreErrors||[],...e.ignoreErrors]),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls]),e.includePaths&&(this.config.includePaths=[...this.config.includePaths||[],...e.includePaths])}shouldIgnoreError(e){return e?T(e,this.config.ignoreErrors||[]):!1}shouldIgnoreScriptUrl(e){return C(e,this.config.ignoreUrls||[])}shouldIncludePath(e){let t=this.config.includePaths||[];return t.length===0?!0:e.some(n=>t.some(r=>{var i;return typeof r=="string"?(i=n.filename)==null?void 0:i.includes(r):r.test(n.filename||"")}))}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:1;return e>=1?!0:e<=0?!1:Math.random()=1?!0:e<=0?!1:Math.random(){try{if(this.originalOnError&&this.originalOnError.call(window,e,t,n,r,i),t&&this.shouldIgnoreScriptUrl(t))return!1;let s=this.buildErrorEvent(i||e,t,n,r);s&&this.client.captureEvent(s)}catch(s){console.error("[LightSDK] ErrorPlugin global error handler error:",s)}return!1})}setupUnhandledRejection(){typeof window!="undefined"&&(this.onUnhandledRejection=e=>{try{let t=e.reason,n=this.extractUrlFromReason(t);if(n&&this.shouldIgnoreScriptUrl(n))return;let r=this.buildErrorEvent(t);r&&(r.type==="error"&&(r.tags={...r.tags||{},unhandled:"true",promise:"true"}),this.client.captureEvent(r))}catch(t){console.error("[LightSDK] ErrorPlugin unhandledrejection handler error:",t)}},window.addEventListener("unhandledrejection",this.onUnhandledRejection))}extractUrlFromReason(e){if(e instanceof Error&&e.stack){let t=e.stack.match(/at\s+(?:.+?\s+)?\((https?:\/\/[^:]+):\d+:\d+\)/);if(t)return t[1];let n=e.stack.match(/(https?:\/\/[^\s)]+):\d+:\d+/);if(n)return n[1]}return null}setupResourceError(){typeof window!="undefined"&&(this.onResourceError=e=>{var i;let t=e.target;if(!t)return;let n=(i=t.tagName)==null?void 0:i.toLowerCase(),r=t.src||t.href;if(n&&r&&["img","script","link","audio","video"].includes(n))try{if(this.shouldIgnoreScriptUrl(r)||!this.shouldSampleResource())return;let s=`resource:${n}`,a=x(r);this.client.captureEvent({type:"error",level:"warning",message:`Resource load failed: ${n} ${a}`,timestamp:d(),fingerprint:s,tags:{resource_type:n,resource_url:a},exception:{type:"ResourceError",value:`Failed to load ${n} resource`}})}catch(s){console.error("[LightSDK] ErrorPlugin resource error handler error:",s)}},window.addEventListener("error",this.onResourceError,!0))}buildErrorEvent(e,t,n,r){let i=d();if(e instanceof Error){if(this.shouldIgnoreError(e.message))return null;let c=P(e.stack,{captureNodeModules:this.config.captureNodeModules,captureColumn:this.config.captureColumn,relativePathOnly:this.config.relativePathOnly,maxFrames:this.config.maxStackFrames});if(!this.shouldIncludePath(c)||!this.shouldSample())return null;let u=L(e.name,e.message,c);return{type:"error",level:"error",message:e.message,timestamp:i,exception:{type:e.name,value:e.message,stacktrace:{frames:c}},fingerprint:u}}let s=String(e);if(this.shouldIgnoreError(s))return null;let a=[];return t&&a.push({filename:t,lineno:n,colno:this.config.captureColumn?r:void 0,in_app:!0}),!this.shouldIncludePath(a)||!this.shouldSample()?null:{type:"error",level:"error",message:s,timestamp:i,exception:{type:"Error",value:s,stacktrace:{frames:a}}}}destroy(){typeof window!="undefined"&&(this.originalOnError!==void 0&&(window.onerror=this.originalOnError),this.onUnhandledRejection&&window.removeEventListener("unhandledrejection",this.onUnhandledRejection),this.onResourceError&&window.removeEventListener("error",this.onResourceError,!0))}},j=H;var $=class{constructor(){this.name="performance";this.version="1.0.0";this.config={sampleRate:.1,captureLongTasks:!0,captureResources:!1,resourceSampleRate:.01,longTaskSampleRate:.05,captureNavigation:!0,captureFP:!0};this.observers=[];this.navLoadTimer=null;this.totalBlockingTime=0;this.fcpTime=null;this.lastLongTaskEndTime=0;this.ttiReported=!1;this.ttiTimer=null}setup(e){this.client=e,this.loadConfig(),this.observeWebVitals(),this.observeLongTasks(),this.observeNavigation(),this.observeResources()}loadConfig(){let e=this.client.config;e&&y(e.performance)&&(this.config={...this.config,...e.performance})}shouldSample(e="default"){var n,r,i;let t=(n=this.config.sampleRate)!=null?n:.1;return e==="resource"?t=(r=this.config.resourceSampleRate)!=null?r:.01:e==="longtask"?t=(i=this.config.longTaskSampleRate)!=null?i:.05:e==="vitals"&&(t=1),t>=1?!0:t<=0?!1:Math.random(){let n=t.getEntriesByName("first-paint");n.length>0&&this.reportMetric("FP",n[0].startTime,"ms",{},"vitals")});e.observe({type:"paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeLCP(){try{let e=new PerformanceObserver(t=>{let n=t.getEntries(),r=n[n.length-1];if(r){let i=r.renderTime||r.loadTime||r.startTime;this.reportMetric("LCP",i,"ms",{},"vitals")}});e.observe({type:"largest-contentful-paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeFID(){try{let e=new PerformanceObserver(t=>{let n=t.getEntries();if(n.length>0){let r=n[0],i=r.processingStart-r.startTime;this.reportMetric("FID",i,"ms",{},"vitals")}});e.observe({type:"first-input",buffered:!0}),this.observers.push(e)}catch(e){}}observeCLS(){try{let e=0,t=0,n=[],r=new PerformanceObserver(i=>{let s=i.getEntries();for(let a of s)if(!a.hadRecentInput){let c=n[0],u=n[n.length-1];t&&a.startTime-((u==null?void 0:u.duration)||0)<1e3&&a.startTime-c.startTime<5e3?(t+=a.value||0,n.push(a)):(t=a.value||0,n=[a]),t>e&&(e=t,this.reportMetric("CLS",e,"",{},"vitals"))}});r.observe({type:"layout-shift",buffered:!0}),this.observers.push(r)}catch(e){}}observeFCP(){try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByName("first-contentful-paint");n.length>0&&(this.fcpTime=n[0].startTime,this.lastLongTaskEndTime=n[0].startTime,this.reportMetric("FCP",n[0].startTime,"ms",{},"vitals"),this.scheduleTTI())});e.observe({type:"paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeTTFB(){try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByType("navigation");if(n.length>0){let r=n[0],i=r.responseStart-r.requestStart;this.reportMetric("TTFB",Math.max(0,i),"ms",{},"vitals")}});e.observe({type:"navigation",buffered:!0}),this.observers.push(e)}catch(e){}}observeLongTasks(){if(this.config.captureLongTasks)try{let e=new PerformanceObserver(t=>{let n=t.getEntries();for(let r of n){let i=r.startTime+r.duration;if(i>this.lastLongTaskEndTime&&(this.lastLongTaskEndTime=i),this.fcpTime!==null&&r.startTime>=this.fcpTime){let s=r.duration-50;s>0&&(this.totalBlockingTime+=s)}this.reportMetric("longtask",r.duration,"ms",{},"longtask"),this.scheduleTTI()}});e.observe({type:"longtask",buffered:!0}),this.observers.push(e)}catch(e){}}scheduleTTI(){this.ttiReported||(this.ttiTimer&&clearTimeout(this.ttiTimer),this.ttiTimer=setTimeout(()=>{if(this.ttiReported)return;this.ttiReported=!0;let e=Math.max(this.lastLongTaskEndTime,this.fcpTime||0);this.reportMetric("TTI",e,"ms",{},"vitals"),this.reportMetric("TBT",this.totalBlockingTime,"ms",{},"vitals")},5e3))}observeNavigation(){this.config.captureNavigation&&typeof performance!="undefined"&&(this.onLoad=()=>{this.navLoadTimer=setTimeout(()=>{let e=performance.timing;if(!e)return;let t=e.navigationStart,n={dom_ready:e.domContentLoadedEventEnd-t,load_time:e.loadEventEnd-t,dns:e.domainLookupEnd-e.domainLookupStart,tcp:e.connectEnd-e.connectStart,ssl:e.secureConnectionStart>0?e.connectEnd-e.secureConnectionStart:0,ttfb:e.responseStart-e.requestStart,download:e.responseEnd-e.responseStart,dom_parse:e.domInteractive-e.responseEnd};for(let[r,i]of Object.entries(n))i>0&&this.reportMetric(r,i,"ms");this.navLoadTimer=null},0)},window.addEventListener("load",this.onLoad))}observeResources(){if(this.config.captureResources&&typeof performance!="undefined")try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByType("resource");for(let r of n)r.duration>1e3&&this.reportMetric("resource_slow",r.duration,"ms",{resource_name:r.name.substring(0,200),resource_type:r.initiatorType},"resource")});e.observe({type:"resource",buffered:!0}),this.observers.push(e)}catch(e){}}getRating(e,t){let r={LCP:{good:2500,poor:4e3},FCP:{good:1800,poor:3e3},FP:{good:1800,poor:3e3},FID:{good:100,poor:300},CLS:{good:.1,poor:.25},TTFB:{good:800,poor:1800}}[e];return!r||t<=r.good?"good":t<=r.poor?"needs-improvement":"poor"}reportMetric(e,t,n,r={},i="default"){if(!this.shouldSample(i))return;let s=this.getRating(e,t),a={type:"performance",level:"info",metric:e,value:Math.round(t*100)/100,unit:n,rating:s,timestamp:d(),tags:{metric:e,rating:s,...r}};this.client.captureEvent(a)}destroy(){for(let e of this.observers)try{e.disconnect()}catch(t){}this.observers=[],this.ttiTimer&&(clearTimeout(this.ttiTimer),this.ttiTimer=null),this.navLoadTimer&&(clearTimeout(this.navLoadTimer),this.navLoadTimer=null),typeof window!="undefined"&&this.onLoad&&window.removeEventListener("load",this.onLoad)}},W=$;var X=class{constructor(){this.name="network";this.version="1.0.0";this.config={captureXHR:!0,captureFetch:!0,captureSuccess:!1,captureBody:!1,captureRequestBody:!1,captureResponseBody:!1,captureRequestHeaders:["content-type","user-agent"],captureResponseHeaders:["content-type","content-length"],errorSampleRate:1,successSampleRate:.01,slowThreshold:3e3,slowSampleRate:1,ignoreStatusCodes:[]}}setup(e){this.client=e,this.loadConfig(),this.config.captureFetch&&this.patchFetch(),this.config.captureXHR&&this.patchXHR()}loadConfig(){let e=this.client.config;e&&y(e.network)&&(this.config={...this.config,...e.network}),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnore(e){return C(e,this.config.ignoreUrls||[])}shouldSample(e,t){var r,i,s,a;let n=e?(r=this.config.errorSampleRate)!=null?r:1:(i=this.config.successSampleRate)!=null?i:.01;return!e&&t>=((s=this.config.slowThreshold)!=null?s:3e3)&&(n=(a=this.config.slowSampleRate)!=null?a:1),n>=1?!0:n<=0?!1:Math.random(){var h;let p=d()-r,w=u.ok,f=u.status;return(h=e.config.ignoreStatusCodes)!=null&&h.includes(f)||!e.shouldSample(!w,p)||(!w||e.config.captureSuccess)&&e.reportNetwork({sub_type:"fetch",method:s.toUpperCase(),url:a,status_code:f,duration:p,success:w,error:w?void 0:`HTTP ${f}`}),u},u=>{let p=d()-r;throw e.shouldSample(!0,p)&&e.reportNetwork({sub_type:"fetch",method:s.toUpperCase(),url:a,duration:p,success:!1,error:(u==null?void 0:u.message)||"Network error"}),u}),c}}patchXHR(){if(typeof XMLHttpRequest=="undefined")return;this.originalXHROpen=XMLHttpRequest.prototype.open,this.originalXHRSend=XMLHttpRequest.prototype.send;let e=this;XMLHttpRequest.prototype.open=function(t,n){return this._lightMethod=t,this._lightUrl=typeof n=="string"?n:n.toString(),this._lightTracked=!1,e.originalXHROpen.apply(this,arguments)},XMLHttpRequest.prototype.send=function(t){let n=d(),r=this._lightMethod||"GET",i=this._lightUrl||"",s=this._lightTracked;if(e.shouldIgnore(i)||s)return e.originalXHRSend.apply(this,arguments);this._lightTracked=!0;let a=x(i),c=0;t&&typeof t=="string"&&(c=t.length);let u=this.onloadend;return this.onloadend=function(p){var v;let w=d()-n,f=this.status,h=f>=200&&f<300;if(!((v=e.config.ignoreStatusCodes)!=null&&v.includes(f))&&e.shouldSample(!h,w)&&(!h||e.config.captureSuccess)){let g=0;try{let E=this.getResponseHeader("content-length");E&&(g=parseInt(E,10)),!g&&this.responseText&&(g=this.responseText.length)}catch(E){}e.reportNetwork({sub_type:"xhr",method:r.toUpperCase(),url:a,status_code:f,duration:w,request_size:c||void 0,response_size:g||void 0,success:h,error:h?void 0:`HTTP ${f}`})}if(u)return u.call(this,p)},e.originalXHRSend.apply(this,arguments)}}reportNetwork(e){let t={type:"network",level:e.success?"info":"error",timestamp:d(),tags:{method:e.method,sub_type:e.sub_type,success:String(e.success)},...e};this.client.captureEvent(t)}destroy(){this.originalFetch&&(window.fetch=this.originalFetch),this.originalXHROpen&&(XMLHttpRequest.prototype.open=this.originalXHROpen),this.originalXHRSend&&(XMLHttpRequest.prototype.send=this.originalXHRSend)}},A=X;var K=class{constructor(){this.name="behavior";this.version="1.0.0";this.config={capturePV:!0,captureClick:!0,captureRoute:!0,captureDuration:!0,captureScroll:!0,clickThrottle:300,scrollThrottle:1e3,sampleRate:.01};this.lastClickTime=0;this.lastScrollTime=0;this.maxScrollDepth=0;this.pageEnterTime=0;this.scrollReported=!1}setup(e){this.client=e,this.loadConfig(),this.config.capturePV&&this.trackPV(),this.config.captureClick&&this.trackClick(),this.config.captureRoute&&this.trackRoute(),this.config.captureDuration&&this.trackPageDuration(),this.config.captureScroll&&this.trackScroll()}loadConfig(){let e=this.client.config;e&&y(e.behavior)&&(this.config={...this.config,...e.behavior})}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:.01;return e>=1?!0:e<=0?!1:Math.random(){var a,c,u;let t=d(),n=(a=this.config.clickThrottle)!=null?a:300;if(t-this.lastClickTime{let r=b();if(r!==e){let i=e,s=r;e=r,this.reportBehavior({sub_type:"route",page_url:s,referrer:i,properties:{from:i,to:s}}),this.pageEnterTime=d(),this.maxScrollDepth=0,this.scrollReported=!1}};this.originalPushState=history.pushState,this.originalReplaceState=history.replaceState;let n=this;history.pushState=function(){let r=n.originalPushState.apply(this,arguments);return setTimeout(t,0),r},history.replaceState=function(){let r=n.originalReplaceState.apply(this,arguments);return setTimeout(t,0),r},this.onPopState=()=>{setTimeout(t,0)},this.onHashchange=()=>{setTimeout(t,0)},window.addEventListener("popstate",this.onPopState),window.addEventListener("hashchange",this.onHashchange)}trackPageDuration(){if(typeof window=="undefined")return;this.pageEnterTime=d();let e=()=>{let t=d()-this.pageEnterTime;t>1e3&&(this.config.captureScroll&&!this.scrollReported&&this.reportScroll(),this.reportBehavior({sub_type:"duration",page_url:b(),properties:{duration:t,max_scroll_depth:this.maxScrollDepth}}))};this.onVisibilityChange=()=>{document.hidden?e():this.pageEnterTime=d()},this.onBeforeUnload=e,this.onPagehide=e,document.addEventListener("visibilitychange",this.onVisibilityChange),window.addEventListener("beforeunload",this.onBeforeUnload),window.addEventListener("pagehide",this.onPagehide)}trackScroll(){typeof window!="undefined"&&(this.onScroll=()=>{var r;let e=d(),t=(r=this.config.scrollThrottle)!=null?r:1e3;if(e-this.lastScrollTimethis.maxScrollDepth&&(this.maxScrollDepth=n)},window.addEventListener("scroll",this.onScroll,{passive:!0}))}reportScroll(){this.scrollReported||(this.scrollReported=!0,this.reportBehavior({sub_type:"scroll",page_url:b(),properties:{max_depth:this.maxScrollDepth}}))}reportBehavior(e){if(!this.shouldSample())return;let t={type:"behavior",level:"info",timestamp:d(),tags:{sub_type:e.sub_type,page_url:b()},...e};this.client.captureEvent(t)}destroy(){typeof window!="undefined"&&(this.originalPushState&&(history.pushState=this.originalPushState),this.originalReplaceState&&(history.replaceState=this.originalReplaceState),this.onPopState&&window.removeEventListener("popstate",this.onPopState),this.onHashchange&&window.removeEventListener("hashchange",this.onHashchange),this.onBeforeUnload&&window.removeEventListener("beforeunload",this.onBeforeUnload),this.onPagehide&&window.removeEventListener("pagehide",this.onPagehide),this.onScroll&&window.removeEventListener("scroll",this.onScroll)),typeof document!="undefined"&&(this.onClick&&document.removeEventListener("click",this.onClick,!0),this.onVisibilityChange&&document.removeEventListener("visibilitychange",this.onVisibilityChange))}},V=K;var _e="light-sentry-offline",m="events",z=1e3,Q=class{constructor(){this.name="offline";this.version="1.0.0";this.config={maxEvents:z};this.db=null;this.isOnline=!0;this.isSyncing=!1;this.pendingEvents=[]}setup(e){this.client=e,this.loadConfig(),this.initDatabase(),this.setupNetworkListeners()}loadConfig(){let e=this.client.config;e&&typeof e.offline=="object"&&(this.config={...this.config,...e.offline})}async initDatabase(){if(typeof indexedDB=="undefined"){console.warn("[LightSDK] OfflinePlugin: IndexedDB not available");return}return new Promise((e,t)=>{let n=indexedDB.open(_e,1);n.onerror=()=>{console.warn("[LightSDK] OfflinePlugin: Failed to open IndexedDB"),t(n.error)},n.onsuccess=()=>{this.db=n.result,e()},n.onupgradeneeded=r=>{let i=r.target.result,s=r.oldVersion;i.objectStoreNames.contains(m)||i.createObjectStore(m,{keyPath:"id"}).createIndex("timestamp","timestamp",{unique:!1})}})}setupNetworkListeners(){typeof window!="undefined"&&(this.isOnline=navigator.onLine,this.onOnline=()=>{console.info("[LightSDK] OfflinePlugin: Network online"),this.isOnline=!0,this.syncPendingEvents()},this.onOffline=()=>{console.info("[LightSDK] OfflinePlugin: Network offline"),this.isOnline=!1},window.addEventListener("online",this.onOnline),window.addEventListener("offline",this.onOffline))}generateId(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}async saveToIndexedDB(e){if(!this.db)return;let t=this.config.maxEvents||z,n=await this.getEventCount();return n>=t&&await this.deleteOldestEvents(n-t+1),new Promise((r,i)=>{let a=this.db.transaction([m],"readwrite").objectStore(m),c={id:this.generateId(),event:{...e,offline:!0},timestamp:d()},u=a.add(c);u.onsuccess=()=>r(),u.onerror=()=>i(u.error)})}getEventCount(){return this.db?new Promise((e,t)=>{let i=this.db.transaction([m],"readonly").objectStore(m).count();i.onsuccess=()=>e(i.result),i.onerror=()=>t(i.error)}):Promise.resolve(0)}async deleteOldestEvents(e){if(!(!this.db||e<=0))return new Promise((t,n)=>{let a=this.db.transaction([m],"readwrite").objectStore(m).index("timestamp").openCursor(),c=0;a.onsuccess=u=>{let p=u.target.result;p&&cn(a.error)})}async getAllStoredEvents(){return this.db?new Promise((e,t)=>{let s=this.db.transaction([m],"readonly").objectStore(m).index("timestamp").getAll();s.onsuccess=()=>{let a=s.result||[];a.sort((c,u)=>c.timestamp-u.timestamp),e(a)},s.onerror=()=>t(s.error)}):[]}async clearStoredEvents(){if(this.db)return new Promise((e,t)=>{let i=this.db.transaction([m],"readwrite").objectStore(m).clear();i.onsuccess=()=>e(),i.onerror=()=>t(i.error)})}async syncPendingEvents(){if(!(!this.isOnline||this.isSyncing)){this.isSyncing=!0,console.info("[LightSDK] OfflinePlugin: Syncing pending events");try{let e=0;for(;this.pendingEvents.length>0;){let n=this.pendingEvents.shift();try{this.client.captureEvent(n),e++}catch(r){console.error("[LightSDK] OfflinePlugin: Failed to re-enqueue memory event",r)}}let t=await this.getAllStoredEvents();if(t.length>0)for(let n of t)try{let r=n.event;this.client.captureEvent(r),e++}catch(r){console.error("[LightSDK] OfflinePlugin: Failed to re-enqueue event",r)}try{await this.client.flush()}catch(n){}await this.clearStoredEvents(),console.info(`[LightSDK] OfflinePlugin: Synced ${e} events`)}catch(e){console.error("[LightSDK] OfflinePlugin: Sync failed",e)}finally{this.isSyncing=!1}}}beforeReport(e){if(!this.isOnline){let n=this.config.maxEvents||z;return this.pendingEvents.length>=n&&this.pendingEvents.shift(),this.pendingEvents.push(e),this.savePendingEventsToDB(),null}let t={...e};return delete t.offline,t}savePendingEventsToDB(){if(this.pendingEvents.length===0)return;let e=this.pendingEvents.splice(0,this.pendingEvents.length),t=0,n=()=>{if(t>=e.length)return;let r=e[t];this.saveToIndexedDB(r).then(()=>{t++,n()}).catch(i=>{console.error("[LightSDK] OfflinePlugin: Failed to save event offline",i),this.pendingEvents.unshift(...e.slice(t))})};n()}destroy(){typeof window!="undefined"&&(this.onOnline&&window.removeEventListener("online",this.onOnline),this.onOffline&&window.removeEventListener("offline",this.onOffline)),this.db&&(this.db.close(),this.db=null)}},G=Q;var J=class{constructor(){this.name="sampling";this.version="1.0.0";this.config={rates:{error:1,performance:1,network:1,behavior:1}}}setup(e){this.client=e,this.loadConfig()}loadConfig(){let e=this.client.config;if(e&&y(e.sampling)){let t=e.sampling;"rates"in t&&y(t.rates)&&(this.config.rates={...this.config.rates,...t.rates})}}shouldSample(e){var r;let t=(r=this.client.config.sampleRate)!=null?r:1;if(t<=0)return!1;let n=this.getTypeRate(e);return t>=1?this.randomCheck(n):this.randomCheck(t*n)}getTypeRate(e){let t=this.config.rates||{},n=e.type;if(n==="count")return 1;let r=t[n];return r!==void 0?r:1}randomCheck(e){return e>=1?!0:e<=0?!1:Math.random()typeof i=="string"))||[];(n.includes("performance")||n.includes("all"))&&t.push(new W),(n.includes("network")||n.includes("all"))&&t.push(new A),(n.includes("behavior")||n.includes("all"))&&t.push(new V);for(let i of t)e.use(i);if(o.plugins)for(let i of o.plugins)typeof i=="object"&&i!==null&&"name"in i&&"setup"in i&&e.use(i);return e.init(),l=e,e}function ce(){return l}function ue(o){l==null||l.captureException(o)}function le(o,e){l==null||l.captureMessage(o,e)}function de(o){l==null||l.captureEvent(o)}function pe(o){l==null||l.setUser(o)}function he(o,e){l==null||l.setTag(o,e)}function fe(o){l==null||l.setTags(o)}function ge(o,e){l==null||l.setExtra(o,e)}function me(o){l==null||l.addBreadcrumb(o)}async function ve(){await(l==null?void 0:l.flush())}function ye(){l==null||l.disable()}function be(){l==null||l.enable()}var Oe={init:ae,getClient:ce,captureException:ue,captureMessage:le,captureEvent:de,setUser:pe,setTag:he,setTags:fe,setExtra:ge,addBreadcrumb:me,flush:ve,disable:ye,enable:be};
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 86a8aff..1a85cc2 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -4,6 +4,7 @@ import PerformancePlugin from './plugins/PerformancePlugin';
import NetworkPlugin from './plugins/NetworkPlugin';
import BehaviorPlugin from './plugins/BehaviorPlugin';
import OfflinePlugin from './plugins/OfflinePlugin';
+import SamplingPlugin from './plugins/SamplingPlugin';
import type { LightConfig, LightClient, SentryEvent, EventLevel, UserInfo, Breadcrumb, LightPlugin, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, DSNInfo } from './types';
declare function init(config: LightConfig): LightClient;
declare function getClient(): LightClient | null;
@@ -20,7 +21,7 @@ declare function addBreadcrumb(breadcrumb: Omit): void;
declare function flush(): Promise;
declare function disable(): void;
declare function enable(): void;
-export { init, getClient, captureException, captureMessage, captureEvent, setUser, setTag, setTags, setExtra, addBreadcrumb, flush, disable, enable, Client, ErrorPlugin, PerformancePlugin, NetworkPlugin, BehaviorPlugin, OfflinePlugin, };
+export { init, getClient, captureException, captureMessage, captureEvent, setUser, setTag, setTags, setExtra, addBreadcrumb, flush, disable, enable, Client, ErrorPlugin, PerformancePlugin, NetworkPlugin, BehaviorPlugin, OfflinePlugin, SamplingPlugin, };
export type { LightConfig, LightClient, LightPlugin, SentryEvent, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, EventLevel, UserInfo, Breadcrumb, DSNInfo, };
declare const _default: {
init: typeof init;
diff --git a/dist/index.esm.js b/dist/index.esm.js
index 2776006..b56b8c5 100644
--- a/dist/index.esm.js
+++ b/dist/index.esm.js
@@ -1,4 +1,3 @@
-function q(o){try{let e=o.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);if(!e)throw new Error("Invalid DSN format");let[,t,n,r,i]=e;return{protocol:t,publicKey:n,host:r,projectId:i}}catch(e){throw new Error(`Failed to parse DSN: ${e.message}`)}}function b(o){return`${o.protocol}://${o.host}/${o.projectId}/envelope/`}function p(){return Date.now()}function v(o){return o!==null&&typeof o=="object"&&!Array.isArray(o)}function T(o){let e=0;for(let t=0;t`${i.filename}:${i.lineno||0}`).join("|"),r=e.replace(/['"]?\d+['"]?/g,"").replace(/\s+/g," ").trim();return T(`${o}:${r}:${n}`)}function G(o){let e=[{name:"WeChat",pattern:/MicroMessenger\/([\d.]+)/i},{name:"QQ Browser",pattern:/QIBrowser\/([\d.]+)/i},{name:"UC Browser",pattern:/UCBrowser\/([\d.]+)/i},{name:"Edge",pattern:/Edg\/([\d.]+)/i},{name:"Edge",pattern:/Edge\/([\d.]+)/i},{name:"Opera",pattern:/OPR\/([\d.]+)/i},{name:"Opera",pattern:/Opera\/([\d.]+)/i},{name:"Firefox",pattern:/Firefox\/([\d.]+)/i},{name:"Safari",pattern:/Version\/([\d.]+).*Safari/i},{name:"Chrome",pattern:/Chrome\/([\d.]+)/i},{name:"IE",pattern:/MSIE ([\d.]+)/i},{name:"IE",pattern:/Trident.*rv:([\d.]+)/i}];for(let t of e){let n=o.match(t.pattern);if(n){let r=n[1]||"",i=r.split(".")[0]||"";return{name:t.name,version:r,major:i}}}return{name:"Unknown",version:"",major:""}}function J(o){let e=[{name:"Windows 11",pattern:/Windows NT 10\.0.*Win64.*x64/i},{name:"Windows 10",pattern:/Windows NT 10\.0/i},{name:"Windows 8.1",pattern:/Windows NT 6\.3/i},{name:"Windows 8",pattern:/Windows NT 6\.2/i},{name:"Windows 7",pattern:/Windows NT 6\.1/i},{name:"Windows Vista",pattern:/Windows NT 6\.0/i},{name:"Windows XP",pattern:/Windows NT 5\.1/i},{name:"macOS",pattern:/Mac OS X ([\d_.]+)/i},{name:"iOS",pattern:/OS ([\d_.]+) like Mac OS X/i},{name:"Android",pattern:/Android ([\d.]+)/i},{name:"Android",pattern:/Android/i},{name:"Linux",pattern:/Linux/i}];for(let t of e){let n=o.match(t.pattern);if(n){let r=n[1]||"";return r&&(r=r.replace(/_/g,".")),{name:t.name,version:r}}}return{name:"Unknown",version:""}}function Z(o){let e=/Mobile|Android|iPhone|iPod|BlackBerry|Windows Phone|Opera Mini/i.test(o),t=/Tablet|iPad|PlayBook|Kindle|Silk/i.test(o),n="desktop";t?n="tablet":e&&(n="mobile");let r="",i="",s=o.match(/iPhone/i),a=o.match(/iPad/i),c=o.match(/SM-([A-Z0-9]+)/i),u=o.match(/Pixel ([0-9A-Z]+)/i),d=o.match(/HUAWEI ([A-Z0-9]+)/i);return s?(r="Apple",i="iPhone"):a?(r="Apple",i="iPad"):c?(r="Samsung",i=c[1]):u?(r="Google",i=`Pixel ${u[1]}`):d&&(r="Huawei",i=d[1]),{type:n,vendor:r,model:i}}function U(){let o=typeof navigator!="undefined"?navigator.userAgent:"",e=G(o),t=J(o),n=Z(o);return{browser:e,os:t,device:n,url:typeof location!="undefined"?location.href:"",referrer:typeof document!="undefined"?document.referrer:"",title:typeof document!="undefined"?document.title:"",language:typeof navigator!="undefined"?navigator.language:"",user_agent:o,screen_width:typeof screen!="undefined"?screen.width:0,screen_height:typeof screen!="undefined"?screen.height:0,viewport_width:typeof window!="undefined"?window.innerWidth:0,viewport_height:typeof window!="undefined"?window.innerHeight:0}}function g(){return typeof location!="undefined"?location.href:""}function H(){return typeof document!="undefined"?document.referrer:""}function C(o){if(!o)return o;try{let e=new URL(o),t=["password","passwd","pwd","token","access_token","refresh_token","api_key","apikey","key","secret","private_key","code","auth","id_token","idToken","session","session_id"],n=e.searchParams,r=!1;for(let i of Array.from(n.keys())){let s=i.toLowerCase().replace(/[-_]/g,"_");t.some(a=>s.includes(a))&&(n.set(i,"[Filtered]"),r=!0)}return r?e.toString():o}catch(e){return o}}function w(o,e=[]){return!e||e.length===0?!1:e.some(t=>{if(typeof t=="string"){if(t.includes("*")){let n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(n).test(o)}return o.includes(t)}return t.test(o)})}var Y={Chrome:"c",Safari:"s",Firefox:"f",Edge:"e",Opera:"o",IE:"i",WeChat:"w","QQ Browser":"q","UC Browser":"u","Mobile Chrome":"cm","Mobile Safari":"sm"},ee={"Windows 11":"w11","Windows 10":"w10","Windows 8.1":"w81","Windows 8":"w8","Windows 7":"w7","Windows Vista":"wv","Windows XP":"wxp",macOS:"m",iOS:"i",Android:"a",Linux:"l"};function M(){let o=U(),e=Y[o.browser.name]||o.browser.name.toLowerCase(),t=ee[o.os.name]||o.os.name.toLowerCase();return{browser:{name:e,version:o.browser.major},os:{name:t,version:o.os.version.split(".")[0]},device:{type:o.device.type,brand:o.device.vendor||void 0,model:o.device.model||void 0}}}function $(){let o=U(),e=`${o.browser.name}|${o.browser.major}|${o.os.name}|${o.os.version.split(".")[0]}|${o.device.type}|${o.device.vendor}|${o.device.model}|${o.language}`;return T(e).substring(0,16)}var te={enabled:!0,sampleRate:1,maxQueueSize:100,flushInterval:5e3,maxRetries:3,retryDelay:1e3,environment:"production",contextLevel:{fatal:{maxStackFrames:5,maxBreadcrumbs:20},error:{maxStackFrames:5,maxBreadcrumbs:10},warning:{maxStackFrames:3,maxBreadcrumbs:5},info:{maxStackFrames:0,maxBreadcrumbs:0},debug:{maxStackFrames:0,maxBreadcrumbs:0}}},S=class{constructor(e){this.contexts=null;this.contextId=null;this.contextReported=!1;this.config={...te,...e}}markContextReported(){this.contextReported=!0}get(e){return this.config[e]}getAll(){return{...this.config}}set(e,t){this.config[e]=t}setUser(e){this.config.user=e||void 0}setTags(e){this.config.tags={...this.config.tags||{},...e}}setTag(e,t){this.config.tags||(this.config.tags={}),this.config.tags[e]=t}setExtra(e,t){this.config.extra||(this.config.extra={}),this.config.extra[e]=t}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:1;return e>=1?!0:e<=0?!1:Math.random()typeof n=="string"?e.includes(n):n.test(e))}getEncodedContext(){return this.contexts||(this.contexts=M()),this.contexts}applyToEvent(e,t=!0){var n,r,i,s;return this.config.release&&(e.release=this.config.release),this.config.environment&&(e.environment=this.config.environment),this.config.user&&(e.user=this.config.user),this.config.tags&&(e.tags={...this.config.tags,...e.tags||{}}),e.request?e.request.url||(e.request.url=g()):e.request={url:g(),referrer:H()},this.contextId||(this.contextId=$()),e.context_id=this.contextId,this.applyContextLevel(e),t&&!this.contextReported&&(this.contexts||(this.contexts=M()),e.contexts=this.contexts,e.env={b:(n=this.contexts.browser)==null?void 0:n.name,bv:(r=this.contexts.browser)==null?void 0:r.version,os:(i=this.contexts.os)==null?void 0:i.name,osv:(s=this.contexts.os)==null?void 0:s.version}),e}applyContextLevel(e){var i,s,a,c;let t=this.config.contextLevel;if(!t)return;let n=e.level||"error",r=t[n]||t.error;(s=(i=e.exception)==null?void 0:i.stacktrace)!=null&&s.frames&&r.maxStackFrames>0?e.exception.stacktrace.frames=e.exception.stacktrace.frames.slice(0,r.maxStackFrames):r.maxStackFrames===0&&((c=(a=e.exception)==null?void 0:a.stacktrace)!=null&&c.frames)&&delete e.exception.stacktrace,e.breadcrumbs&&r.maxBreadcrumbs>0?e.breadcrumbs=e.breadcrumbs.slice(-r.maxBreadcrumbs):r.maxBreadcrumbs===0&&delete e.breadcrumbs}};var x=class{constructor(){this.handlers=new Map}on(e,t){this.handlers.has(e)||this.handlers.set(e,[]),this.handlers.get(e).push(t)}off(e,t){let n=this.handlers.get(e);if(n){let r=n.indexOf(t);r>-1&&n.splice(r,1)}}emit(e,...t){let n=this.handlers.get(e);if(n)for(let r of n)try{r(...t)}catch(i){console.error("[LightSDK] EventBus handler error:",i)}}once(e,t){let n=(...r)=>{this.off(e,n),t(...r)};this.on(e,n)}destroy(){this.handlers.clear()}};var R=class{constructor(e,t,n,r,i){this.queue=[];this.timer=null;this.lastFlushTime=0;this.dedupeMap=new Map;this.pendingCounts=new Map;this.errorRateWindow={};this.paused=!1;this.pauseTimer=null;this.maxSize=e,this.flushInterval=t,this.eventBus=n,this.flushCallback=r,this.syncFlushCallback=i}start(){this.startTimer(),this.setupVisibilityListener()}startTimer(){this.timer||(this.timer=setInterval(()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flush()},this.flushInterval))}setupVisibilityListener(){typeof document!="undefined"&&document.addEventListener("visibilitychange",()=>{document.hidden&&(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}),typeof window!="undefined"&&(window.addEventListener("beforeunload",()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}),window.addEventListener("pagehide",()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}))}checkInfiniteLoop(e){let t=p(),n=t-1e3;this.errorRateWindow[e]||(this.errorRateWindow[e]=[]);let r=this.errorRateWindow[e];for(r.push(t);r.length>0&&r[0]10?(this.paused||(console.warn("[LightSDK] Infinite loop detected, pausing SDK for 60s",{fingerprint:e,count:r.length}),this.eventBus.emit("error",new Error("Infinite loop detected")),this.paused=!0,this.pauseTimer&&clearTimeout(this.pauseTimer),this.pauseTimer=setTimeout(()=>{this.paused=!1,this.errorRateWindow={},console.info("[LightSDK] Resumed after infinite loop detection")},6e4)),!0):!1}enqueue(e){if(this.paused)return;let t=this.getDedupeKey(e);if(t){if(this.checkInfiniteLoop(t))return;let n=p(),r=this.dedupeMap.get(t);if(r)if(n-r.lastTime<6e4){if(r.count>=3){if(!this.pendingCounts.has(t))this.pendingCounts.set(t,{count:1,ts_start:n,ts_end:n});else{let i=this.pendingCounts.get(t);i.count++,i.ts_end=n}return}r.count++,r.lastTime=n}else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!0}),this.pendingCounts.delete(t);else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!1})}this.queue.length>=this.maxSize&&this.flush(),this.queue.push(e),this.eventBus.emit("event",e),this.queue.length>=this.maxSize&&this.flush()}buildCountEvents(){let e=[];for(let[t,n]of this.pendingCounts.entries()){let r={type:"count",fingerprint:t,count:n.count,ts_start:n.ts_start,ts_end:n.ts_end,timestamp:p(),level:"info"};e.push(r)}return this.pendingCounts.clear(),e}getDedupeKey(e){if(e.type==="error"){let t=e;return t.fingerprint||t.message}return null}async flush(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=p(),this.eventBus.emit("report",n);try{await this.flushCallback(n),this.eventBus.emit("reported",n)}catch(r){throw e.forEach(i=>this.queue.unshift(i)),r}}flushSync(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=p(),this.eventBus.emit("report",n);try{this.syncFlushCallback(n),this.eventBus.emit("reported",n)}catch(r){console.error("[LightSDK] Sync flush failed",r)}}size(){return this.queue.length}destroy(){this.timer&&(clearInterval(this.timer),this.timer=null),this.pauseTimer&&(clearTimeout(this.pauseTimer),this.pauseTimer=null),this.dedupeMap.clear(),this.pendingCounts.clear(),this.errorRateWindow={}}};var W={event_id:"eid",timestamp:"ts",start_timestamp:"sts",type:"t",level:"l",message:"m",platform:"p",release:"r",environment:"e",fingerprint:"fp",context_id:"cid",user:"u",tags:"tg",exception:"ex",request:"req",contexts:"ctx",breadcrumbs:"bc",transaction:"tx",duration:"d",spans:"sp",extra:"xt"},ke=Object.fromEntries(Object.entries(W).map(([o,e])=>[e,o]));function j(o){if(!o||typeof o!="object")return o;let e={};for(let[t,n]of Object.entries(o)){let r=W[t]||t;e[r]=n}return e}var L=class{constructor(e,t,n){this.useShortFields=!0;this.dsn=e,this.maxRetries=t,this.retryDelay=n}async report(e){if(e.length===0)return;let t=this.buildEnvelope(e),n=0;for(;n<=this.maxRetries;)try{await this.send(t,!1);return}catch(r){let i=r.status;if(i&&i>=400&&i<500||(n++,n>this.maxRetries))throw r;await this.delay(this.retryDelay*Math.pow(2,n-1))}}reportSync(e){if(e.length===0)return;let t=this.buildEnvelope(e);try{if(this.sendSync(t))return}catch(n){}this.sendViaImage(t)}buildEnvelope(e){let t=this.extractSharedMeta(e),n={event_id:this.generateEventId(),sent_at:new Date().toISOString(),meta:t};this.useShortFields&&(n._sf=1);let r=this.applyRelativeTimestamps(e,n),s=[JSON.stringify(n)];for(let a of r){let c=this.stripSharedFields(a,t);this.useShortFields&&(c=j(c));let u=JSON.stringify(c),d=JSON.stringify({type:this.getEnvelopeType(a),length:u.length});s.push(d,u)}return s.join(`
-`)}applyRelativeTimestamps(e,t){if(e.length<=1)return e;let n=[],r=e[0].timestamp?new Date(e[0].timestamp).getTime():Date.now();t._rt=1,t._bt=r;for(let i=0;is.release===t.release)&&(n.release=t.release),t.environment&&e.every(s=>s.environment===t.environment)&&(n.environment=t.environment),t.user&&e.every(s=>JSON.stringify(s.user)===JSON.stringify(t.user))&&(n.user=t.user);let r=t.env;return r&&(n.env=r),n}stripSharedFields(e,t){let n={...e};return t.release&&n.release===t.release&&delete n.release,t.environment&&n.environment===t.environment&&delete n.environment,t.user&&JSON.stringify(n.user)===JSON.stringify(t.user)&&delete n.user,t.env&&delete n.env,n}getEnvelopeType(e){switch(e.type){case"error":return"event";case"performance":return"transaction";default:return e.type}}generateEventId(){return"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".replace(/[x]/g,()=>(Math.random()*16|0).toString(16))}async send(e,t=!1){let n=b(this.dsn);if(navigator.sendBeacon)try{let r=new Blob([e],{type:"application/x-sentry-envelope"});if(navigator.sendBeacon(n,r))return}catch(r){}if(typeof fetch=="function")try{let r=await fetch(n,{method:"POST",body:e,headers:{"Content-Type":"application/x-sentry-envelope"},keepalive:!0});if(r.ok)return;let i=new Error(`HTTP ${r.status}`);throw i.status=r.status,i}catch(r){throw r}if(typeof XMLHttpRequest!="undefined")return new Promise((r,i)=>{let s=new XMLHttpRequest;s.open("POST",n,!t),s.setRequestHeader("Content-Type","application/x-sentry-envelope"),s.onload=()=>{if(s.status>=200&&s.status<300)r();else{let a=new Error(`HTTP ${s.status}`);a.status=s.status,i(a)}},s.onerror=()=>i(new Error("Network error")),s.send(e)});throw new Error("No transport available")}sendSync(e){let t=b(this.dsn);if(navigator.sendBeacon)try{let n=new Blob([e],{type:"application/x-sentry-envelope"});return navigator.sendBeacon(t,n)}catch(n){return!1}if(typeof XMLHttpRequest!="undefined")try{let n=new XMLHttpRequest;return n.open("POST",t,!1),n.setRequestHeader("Content-Type","application/x-sentry-envelope"),n.send(e),n.status>=200&&n.status<300}catch(n){return!1}return!1}sendViaImage(e){try{let t=b(this.dsn),n=new Image,r=encodeURIComponent(btoa(e));return n.src=t+"&sentry_data="+r.substring(0,2e3),!0}catch(t){return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};var k=class{constructor(e){this.plugins=new Map;this.client=e}add(e){if(!this.plugins.has(e.name)){this.plugins.set(e.name,e);try{e.setup(this.client)}catch(t){console.error(`[LightSDK] Plugin "${e.name}" setup error:`,t)}}}remove(e){let t=this.plugins.get(e);if(t){if(t.destroy)try{t.destroy()}catch(n){console.error(`[LightSDK] Plugin "${e}" destroy error:`,n)}this.plugins.delete(e)}}get(e){return this.plugins.get(e)}has(e){return this.plugins.has(e)}applyBeforeReport(e){let t=e;for(let n of this.plugins.values())if(n.beforeReport)try{let r=n.beforeReport(t);if(r===null)return null;t=r}catch(r){console.error(`[LightSDK] Plugin "${n.name}" beforeReport error:`,r)}return t}applyAfterReport(e){for(let t of this.plugins.values())if(t.afterReport)try{t.afterReport(e)}catch(n){console.error(`[LightSDK] Plugin "${t.name}" afterReport error:`,n)}}destroy(){for(let e of this.plugins.values())if(e.destroy)try{e.destroy()}catch(t){console.error(`[LightSDK] Plugin "${e.name}" destroy error:`,t)}this.plugins.clear()}};var I=class{constructor(e){this.breadcrumbs=[];this.maxBreadcrumbs=20;this.enabled=!0;var t,n,r,i;if(!e.dsn)throw new Error("DSN is required");this.dsn=q(e.dsn),this.configManager=new S(e),this.config=this.configManager.getAll(),this.eventBus=new x,this.reporter=new L(this.dsn,(t=e.maxRetries)!=null?t:3,(n=e.retryDelay)!=null?n:1e3),this.pluginManager=new k(this),this.queue=new R((r=e.maxQueueSize)!=null?r:100,(i=e.flushInterval)!=null?i:5e3,this.eventBus,async s=>this.flushEvents(s),s=>this.syncFlushEvents(s)),this.enabled=e.enabled!==!1}init(){this.enabled&&(this.queue.start(),this.emit("ready"))}async flushEvents(e){let t=[];for(let i of e){let s=this.pluginManager.applyBeforeReport(i);s&&t.push(s)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let s=this.configManager.applyToEvent(i,n);n=!1;let a=this.config.get("beforeSend")?this.config.get("beforeSend")(s):s;a&&r.push(a)}if(r.length!==0){await this.reporter.report(r),this.configManager.markContextReported();for(let i of r)this.pluginManager.applyAfterReport(i)}}syncFlushEvents(e){let t=[];for(let i of e){let s=this.pluginManager.applyBeforeReport(i);s&&t.push(s)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let s=this.configManager.applyToEvent(i,n);n=!1;let a=this.config.get("beforeSend")?this.config.get("beforeSend")(s):s;a&&r.push(a)}r.length!==0&&(this.reporter.reportSync(r),this.configManager.markContextReported())}on(e,t){this.eventBus.on(e,t)}off(e,t){this.eventBus.off(e,t)}emit(e,...t){this.eventBus.emit(e,...t)}captureException(e){if(!this.enabled||!this.configManager.shouldSample())return;let t=this.buildErrorEvent(e);this.configManager.isIgnoredError(t.message)||(t.breadcrumbs=[...this.breadcrumbs],this.queue.enqueue(t))}captureMessage(e,t="info"){if(!this.enabled||!this.configManager.shouldSample())return;let n={type:"error",level:t,message:e,timestamp:p(),breadcrumbs:[...this.breadcrumbs]};this.configManager.isIgnoredError(e)||this.queue.enqueue(n)}captureEvent(e){if(!this.enabled||!this.configManager.shouldSample())return;let t={timestamp:p(),level:"info",...e};this.queue.enqueue(t)}buildErrorEvent(e){let t=p();if(e instanceof Error){let r=this.parseStackTrace(e.stack),i=E(e.name,e.message,r);return{type:"error",level:"error",message:e.message,timestamp:t,exception:{type:e.name,value:e.message,stacktrace:{frames:r.slice(0,5)}},fingerprint:i}}return{type:"error",level:"error",message:String(e),timestamp:t}}parseStackTrace(e){if(!e)return[];let t=[],n=e.split(`
-`);for(let r of n){let i=r.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(i){let[,s,a,c,u]=i;t.push({filename:a,function:s,lineno:parseInt(c,10),colno:parseInt(u,10),in_app:!a.includes("node_modules")})}else{let s=r.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(s){let[,a,c,u]=s;t.push({filename:a,lineno:parseInt(c,10),colno:parseInt(u,10),in_app:!a.includes("node_modules")})}}}return t.reverse()}setUser(e){this.configManager.setUser(e)}setTag(e,t){this.configManager.setTag(e,t)}setTags(e){this.configManager.setTags(e)}setExtra(e,t){this.configManager.setExtra(e,t)}addBreadcrumb(e){let t={...e,timestamp:p()};this.breadcrumbs.push(t),this.breadcrumbs.length>this.maxBreadcrumbs&&this.breadcrumbs.shift()}async flush(){await this.queue.flush()}disable(){this.enabled=!1}enable(){this.enabled=!0}use(e){e&&typeof e=="object"&&"name"in e&&"setup"in e&&this.pluginManager.add(e)}destroy(){this.queue.destroy(),this.pluginManager.destroy(),this.eventBus.destroy(),this.breadcrumbs=[]}},K=I;var _=class{constructor(){this.name="error";this.version="1.0.0";this.config={maxStackFrames:5,captureNodeModules:!1,captureColumn:!0,relativePathOnly:!1}}setup(e){this.client=e,this.loadConfig(),this.setupGlobalError(),this.setupUnhandledRejection(),this.setupResourceError()}loadConfig(){let e=this.client.config;e&&v(e.error)&&(this.config={...this.config,...e.error}),e.ignoreErrors&&(this.config.ignoreErrors=[...this.config.ignoreErrors||[],...e.ignoreErrors]),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnoreError(e){return!this.config.ignoreErrors||!e?!1:this.config.ignoreErrors.some(t=>typeof t=="string"?e.includes(t):t.test(e))}shouldIgnoreScriptUrl(e){return w(e,this.config.ignoreUrls)}setupGlobalError(){if(typeof window=="undefined")return;let e=window.onerror;window.onerror=(t,n,r,i,s)=>{try{e&&e.call(window,t,n,r,i,s);let a=this.buildErrorEvent(s||t,n,r,i);a&&this.client.captureEvent(a)}catch(a){console.error("[LightSDK] ErrorPlugin global error handler error:",a)}return!1}}setupUnhandledRejection(){typeof window!="undefined"&&window.addEventListener("unhandledrejection",e=>{try{let t=e.reason,n=this.buildErrorEvent(t);n&&(n.type==="error"&&(n.tags={...n.tags||{},unhandled:"true",promise:"true"}),this.client.captureEvent(n))}catch(t){console.error("[LightSDK] ErrorPlugin unhandledrejection handler error:",t)}})}setupResourceError(){typeof window!="undefined"&&window.addEventListener("error",e=>{var i;let t=e.target;if(!t)return;let n=(i=t.tagName)==null?void 0:i.toLowerCase(),r=t.src||t.href;if(n&&r&&["img","script","link","audio","video"].includes(n))try{this.client.captureEvent({type:"error",level:"warning",message:`Resource load failed: ${n} ${r}`,timestamp:p(),tags:{resource_type:n,resource_url:r},exception:{type:"ResourceError",value:`Failed to load ${n} resource`}})}catch(s){console.error("[LightSDK] ErrorPlugin resource error handler error:",s)}},!0)}buildErrorEvent(e,t,n,r){let i=p();if(e instanceof Error){if(this.shouldIgnoreError(e.message))return null;let c=this.parseStackTrace(e.stack),u=E(e.name,e.message,c);return{type:"error",level:"error",message:e.message,timestamp:i,exception:{type:e.name,value:e.message,stacktrace:{frames:c.slice(0,this.config.maxStackFrames||5)}},fingerprint:u}}let s=String(e);if(this.shouldIgnoreError(s))return null;let a=[];return t&&a.push({filename:t,lineno:n,colno:this.config.captureColumn?r:void 0,in_app:!0}),{type:"error",level:"error",message:s,timestamp:i,exception:{type:"Error",value:s,stacktrace:{frames:a}}}}parseStackTrace(e){if(!e)return[];let t=[],n=e.split(`
-`),r=typeof location!="undefined"?`${location.protocol}//${location.host}`:"";for(let i of n){let s=i.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(s){let[,a,c,u,d]=s,f=c.includes("node_modules");if(!this.config.captureNodeModules&&f)continue;let h=c;this.config.relativePathOnly&&r&&c.startsWith(r)&&(h=c.substring(r.length)),t.push({filename:h,function:a,lineno:parseInt(u,10),colno:this.config.captureColumn?parseInt(d,10):void 0,in_app:!f})}else{let a=i.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(a){let[,c,u,d]=a,f=c.includes("node_modules");if(!this.config.captureNodeModules&&f)continue;let h=c;this.config.relativePathOnly&&r&&c.startsWith(r)&&(h=c.substring(r.length)),t.push({filename:h,lineno:parseInt(u,10),colno:this.config.captureColumn?parseInt(d,10):void 0,in_app:!f})}}}return t.reverse()}destroy(){}},A=_;var B=class{constructor(){this.name="performance";this.version="1.0.0";this.config={sampleRate:.1,captureLongTasks:!0,captureResources:!1,resourceSampleRate:.01,longTaskSampleRate:.05,captureNavigation:!0,captureFP:!0}}setup(e){this.client=e,this.loadConfig(),this.shouldSample("default")&&(this.observeWebVitals(),this.observeLongTasks(),this.observeNavigation(),this.observeResources())}loadConfig(){let e=this.client.config;e&&v(e.performance)&&(this.config={...this.config,...e.performance})}shouldSample(e="default"){var n,r,i;let t=(n=this.config.sampleRate)!=null?n:.1;return e==="resource"?t=(r=this.config.resourceSampleRate)!=null?r:.01:e==="longtask"&&(t=(i=this.config.longTaskSampleRate)!=null?i:.05),t>=1?!0:t<=0?!1:Math.random(){let n=t.getEntriesByName("first-paint");n.length>0&&this.reportMetric("FP",n[0].startTime,"ms")}).observe({type:"paint",buffered:!0})}catch(e){}}observeLCP(){try{new PerformanceObserver(t=>{let n=t.getEntries(),r=n[n.length-1];if(r){let i=r.renderTime||r.loadTime||r.startTime;this.reportMetric("LCP",i,"ms")}}).observe({type:"largest-contentful-paint",buffered:!0})}catch(e){}}observeFID(){try{new PerformanceObserver(t=>{let n=t.getEntries();if(n.length>0){let r=n[0],i=r.processingStart-r.startTime;this.reportMetric("FID",i,"ms")}}).observe({type:"first-input",buffered:!0})}catch(e){}}observeCLS(){try{let e=0,t=0,n=[];new PerformanceObserver(i=>{let s=i.getEntries();for(let a of s)if(!a.hadRecentInput){let c=n[0],u=n[n.length-1];t&&a.startTime-u.endTime<1e3&&a.startTime-c.startTime<5e3?(t+=a.value||0,n.push(a)):(t=a.value||0,n=[a]),t>e&&(e=t,this.reportMetric("CLS",e,""))}}).observe({type:"layout-shift",buffered:!0})}catch(e){}}observeFCP(){try{new PerformanceObserver(t=>{let n=t.getEntriesByName("first-contentful-paint");n.length>0&&this.reportMetric("FCP",n[0].startTime,"ms")}).observe({type:"paint",buffered:!0})}catch(e){}}observeTTFB(){try{new PerformanceObserver(t=>{let n=t.getEntriesByType("navigation");if(n.length>0){let r=n[0],i=r.responseStart-r.requestStart;this.reportMetric("TTFB",Math.max(0,i),"ms")}}).observe({type:"navigation",buffered:!0})}catch(e){}}observeLongTasks(){if(this.config.captureLongTasks)try{new PerformanceObserver(t=>{let n=t.getEntries();for(let r of n)this.shouldSample("longtask")&&this.reportMetric("longtask",r.duration,"ms")}).observe({type:"longtask",buffered:!0})}catch(e){}}observeNavigation(){this.config.captureNavigation&&typeof performance!="undefined"&&window.addEventListener("load",()=>{setTimeout(()=>{let e=performance.timing;if(!e)return;let t=e.navigationStart,n={dom_ready:e.domContentLoadedEventEnd-t,load_time:e.loadEventEnd-t,dns:e.domainLookupEnd-e.domainLookupStart,tcp:e.connectEnd-e.connectStart,ssl:e.secureConnectionStart>0?e.connectEnd-e.secureConnectionStart:0,ttfb:e.responseStart-e.requestStart,download:e.responseEnd-e.responseStart,dom_parse:e.domInteractive-e.responseEnd};for(let[r,i]of Object.entries(n))i>0&&this.reportMetric(r,i,"ms")},0)})}observeResources(){if(this.config.captureResources&&typeof performance!="undefined")try{new PerformanceObserver(t=>{if(!this.shouldSample("resource"))return;let n=t.getEntriesByType("resource");for(let r of n)r.duration>1e3&&this.reportMetric("resource_slow",r.duration,"ms",{resource_name:r.name.substring(0,200),resource_type:r.initiatorType})}).observe({type:"resource",buffered:!0})}catch(e){}}getRating(e,t){let r={LCP:{good:2500,poor:4e3},FCP:{good:1800,poor:3e3},FP:{good:1800,poor:3e3},FID:{good:100,poor:300},CLS:{good:.1,poor:.25},TTFB:{good:800,poor:1800}}[e];return!r||t<=r.good?"good":t<=r.poor?"needs-improvement":"poor"}reportMetric(e,t,n,r={}){let i=this.getRating(e,t),s={type:"performance",level:"info",metric:e,value:Math.round(t*100)/100,unit:n,rating:i,timestamp:p(),tags:{metric:e,rating:i,...r}};this.client.captureEvent(s)}destroy(){}},X=B;var D=class{constructor(){this.name="network";this.version="1.0.0";this.config={captureSuccess:!1,captureBody:!1,captureRequestBody:!1,captureResponseBody:!1,captureRequestHeaders:["content-type","user-agent"],captureResponseHeaders:["content-type","content-length"],errorSampleRate:1,successSampleRate:.01,slowThreshold:3e3,slowSampleRate:1,ignoreStatusCodes:[]}}setup(e){this.client=e,this.loadConfig(),this.patchFetch(),this.patchXHR()}loadConfig(){let e=this.client.config;e&&v(e.network)&&(this.config={...this.config,...e.network}),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnore(e){return w(e,this.config.ignoreUrls||[])}shouldSample(e,t){var r,i,s,a;let n=e?(r=this.config.errorSampleRate)!=null?r:1:(i=this.config.successSampleRate)!=null?i:.01;return!e&&t>=((s=this.config.slowThreshold)!=null?s:3e3)&&(n=(a=this.config.slowSampleRate)!=null?a:1),n>=1?!0:n<=0?!1:Math.random(){var y;let d=p()-r,f=u.ok,h=u.status;return(y=e.config.ignoreStatusCodes)!=null&&y.includes(h)||!e.shouldSample(!f,d)||(!f||e.config.captureSuccess)&&e.reportNetwork({sub_type:"fetch",method:s.toUpperCase(),url:a,status_code:h,duration:d,success:f,error:f?void 0:`HTTP ${h}`}),u},u=>{let d=p()-r;throw e.shouldSample(!0,d)&&e.reportNetwork({sub_type:"fetch",method:s.toUpperCase(),url:a,duration:d,success:!1,error:(u==null?void 0:u.message)||"Network error"}),u}),c}}patchXHR(){if(typeof XMLHttpRequest=="undefined")return;this.originalXHROpen=XMLHttpRequest.prototype.open,this.originalXHRSend=XMLHttpRequest.prototype.send;let e=this;XMLHttpRequest.prototype.open=function(t,n){return this._lightMethod=t,this._lightUrl=typeof n=="string"?n:n.toString(),e.originalXHROpen.apply(this,arguments)},XMLHttpRequest.prototype.send=function(t){let n=p(),r=this._lightMethod||"GET",i=this._lightUrl||"";if(e.shouldIgnore(i))return e.originalXHRSend.apply(this,arguments);let s=C(i),a=0;t&&typeof t=="string"&&(a=t.length);let c=()=>{var y;let u=p()-n,d=this.status,f=d>=200&&d<300;if((y=e.config.ignoreStatusCodes)!=null&&y.includes(d)){this.removeEventListener("loadend",c);return}if(!e.shouldSample(!f,u)){this.removeEventListener("loadend",c);return}if(f&&!e.config.captureSuccess){this.removeEventListener("loadend",c);return}let h=0;try{let P=this.getResponseHeader("content-length");P&&(h=parseInt(P,10)),!h&&this.responseText&&(h=this.responseText.length)}catch(P){}e.reportNetwork({sub_type:"xhr",method:r.toUpperCase(),url:s,status_code:d,duration:u,request_size:a||void 0,response_size:h||void 0,success:f,error:f?void 0:`HTTP ${d}`}),this.removeEventListener("loadend",c)};return this.addEventListener("loadend",c),e.originalXHRSend.apply(this,arguments)}}reportNetwork(e){let t={type:"network",level:e.success?"info":"error",timestamp:p(),tags:{method:e.method,sub_type:e.sub_type,success:String(e.success)},...e};this.client.captureEvent(t)}destroy(){this.originalFetch&&(window.fetch=this.originalFetch),this.originalXHROpen&&(XMLHttpRequest.prototype.open=this.originalXHROpen),this.originalXHRSend&&(XMLHttpRequest.prototype.send=this.originalXHRSend)}},z=D;var O=class{constructor(){this.name="behavior";this.version="1.0.0";this.config={capturePV:!0,captureClick:!0,captureRoute:!0,captureDuration:!0,captureScroll:!0,clickThrottle:300,scrollThrottle:1e3,sampleRate:.1};this.lastClickTime=0;this.lastScrollTime=0;this.maxScrollDepth=0;this.pageEnterTime=0;this.scrollReported=!1}setup(e){this.client=e,this.loadConfig(),this.shouldSample()&&(this.config.capturePV&&this.trackPV(),this.config.captureClick&&this.trackClick(),this.config.captureRoute&&this.trackRoute(),this.config.captureDuration&&this.trackPageDuration(),this.config.captureScroll&&this.trackScroll())}loadConfig(){let e=this.client.config;e&&v(e.behavior)&&(this.config={...this.config,...e.behavior})}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:.1;return e>=1?!0:e<=0?!1:Math.random(){var a,c,u;let t=p(),n=(a=this.config.clickThrottle)!=null?a:300;if(t-this.lastClickTime{let i=g();if(i!==e){let s=e,a=i;e=i,this.reportBehavior({sub_type:"route",page_url:a,referrer:s,properties:{from:s,to:a}}),this.pageEnterTime=p(),this.maxScrollDepth=0,this.scrollReported=!1}},n=history.pushState,r=history.replaceState;history.pushState=function(){let i=n.apply(this,arguments);return setTimeout(t,0),i},history.replaceState=function(){let i=r.apply(this,arguments);return setTimeout(t,0),i},window.addEventListener("popstate",()=>{setTimeout(t,0)}),window.addEventListener("hashchange",()=>{setTimeout(t,0)})}trackPageDuration(){if(typeof window=="undefined")return;this.pageEnterTime=p();let e=()=>{let t=p()-this.pageEnterTime;t>1e3&&(this.config.captureScroll&&!this.scrollReported&&this.reportScroll(),this.reportBehavior({sub_type:"duration",page_url:g(),properties:{duration:t,max_scroll_depth:this.maxScrollDepth}}))};document.addEventListener("visibilitychange",()=>{document.hidden?e():this.pageEnterTime=p()}),window.addEventListener("beforeunload",e),window.addEventListener("pagehide",e)}trackScroll(){if(typeof window=="undefined")return;let e=()=>{var i;let t=p(),n=(i=this.config.scrollThrottle)!=null?i:1e3;if(t-this.lastScrollTimethis.maxScrollDepth&&(this.maxScrollDepth=r)};window.addEventListener("scroll",e,{passive:!0})}reportScroll(){this.scrollReported||(this.scrollReported=!0,this.reportBehavior({sub_type:"scroll",page_url:g(),properties:{max_depth:this.maxScrollDepth}}))}reportBehavior(e){let t={type:"behavior",level:"info",timestamp:p(),tags:{sub_type:e.sub_type,page_url:g()},...e};this.client.captureEvent(t)}destroy(){}},V=O;var ne="light-sentry-offline",m="events",N=1e3,F=class{constructor(){this.name="offline";this.version="1.0.0";this.config={maxEvents:N};this.db=null;this.isOnline=!0;this.isSyncing=!1;this.pendingEvents=[]}setup(e){this.client=e,this.loadConfig(),this.initDatabase(),this.setupNetworkListeners()}loadConfig(){let e=this.client.config;e&&typeof e.offline=="object"&&(this.config={...this.config,...e.offline})}async initDatabase(){if(typeof indexedDB=="undefined"){console.warn("[LightSDK] OfflinePlugin: IndexedDB not available");return}return new Promise((e,t)=>{let n=indexedDB.open(ne,1);n.onerror=()=>{console.warn("[LightSDK] OfflinePlugin: Failed to open IndexedDB"),t(n.error)},n.onsuccess=()=>{this.db=n.result,e()},n.onupgradeneeded=r=>{let i=r.target.result;i.objectStoreNames.contains(m)||i.createObjectStore(m,{keyPath:"id"}).createIndex("timestamp","timestamp",{unique:!1})}})}setupNetworkListeners(){typeof window!="undefined"&&(this.isOnline=navigator.onLine,window.addEventListener("online",()=>{console.info("[LightSDK] OfflinePlugin: Network online"),this.isOnline=!0,this.syncPendingEvents()}),window.addEventListener("offline",()=>{console.info("[LightSDK] OfflinePlugin: Network offline"),this.isOnline=!1}))}generateId(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}async saveToIndexedDB(e){if(this.db)return new Promise((t,n)=>{let i=this.db.transaction([m],"readwrite").objectStore(m),s=i.count();s.onsuccess=async()=>{let a=s.result;a>=(this.config.maxEvents||N)&&await this.deleteOldestEvents(a-(this.config.maxEvents||N)+1);let c={id:this.generateId(),event:{...e,offline:!0},timestamp:p()},u=i.add(c);u.onsuccess=()=>t(),u.onerror=()=>n(u.error)}})}async deleteOldestEvents(e){if(!(!this.db||e<=0))return new Promise((t,n)=>{let a=this.db.transaction([m],"readwrite").objectStore(m).index("timestamp").openCursor(),c=0;a.onsuccess=u=>{let d=u.target.result;d&&cn(a.error)})}async getAllStoredEvents(){return this.db?new Promise((e,t)=>{let s=this.db.transaction([m],"readonly").objectStore(m).index("timestamp").getAll();s.onsuccess=()=>{let a=s.result||[];a.sort((c,u)=>c.timestamp-u.timestamp),e(a)},s.onerror=()=>t(s.error)}):[]}async clearStoredEvents(){if(this.db)return new Promise((e,t)=>{let i=this.db.transaction([m],"readwrite").objectStore(m).clear();i.onsuccess=()=>e(),i.onerror=()=>t(i.error)})}async syncPendingEvents(){if(!(!this.isOnline||this.isSyncing)){this.isSyncing=!0,console.info("[LightSDK] OfflinePlugin: Syncing pending events");try{let e=await this.getAllStoredEvents();if(e.length===0){console.info("[LightSDK] OfflinePlugin: No pending events to sync"),this.isSyncing=!1;return}let t=50;for(let n=0;ns.event);try{await(async()=>new Promise((a,c)=>{this.client.emit("sync:offline",i),a()}))(),console.info(`[LightSDK] OfflinePlugin: Synced ${r.length} events`)}catch(s){console.error("[LightSDK] OfflinePlugin: Failed to sync batch",s)}}await this.clearStoredEvents(),console.info("[LightSDK] OfflinePlugin: All pending events synced")}catch(e){console.error("[LightSDK] OfflinePlugin: Sync failed",e)}finally{this.isSyncing=!1}}}beforeReport(e){if(!this.isOnline)return this.saveToIndexedDB(e).catch(n=>{console.error("[LightSDK] OfflinePlugin: Failed to save event offline",n)}),null;let t={...e};return delete t.offline,t}destroy(){this.db&&(this.db.close(),this.db=null)}},Q=F;var l=null;function re(o){var r;let e=new K(o),t=[new A,new Q],n=((r=o.plugins)==null?void 0:r.filter(i=>typeof i=="string"))||[];(n.includes("performance")||n.includes("all"))&&t.push(new X),(n.includes("network")||n.includes("all"))&&t.push(new z),(n.includes("behavior")||n.includes("all"))&&t.push(new V);for(let i of t)e.use(i);if(o.plugins)for(let i of o.plugins)typeof i=="object"&&i!==null&&"name"in i&&"setup"in i&&e.use(i);return e.init(),l=e,e}function ie(){return l}function se(o){l==null||l.captureException(o)}function oe(o,e){l==null||l.captureMessage(o,e)}function ae(o){l==null||l.captureEvent(o)}function ce(o){l==null||l.setUser(o)}function ue(o,e){l==null||l.setTag(o,e)}function le(o){l==null||l.setTags(o)}function pe(o,e){l==null||l.setExtra(o,e)}function de(o){l==null||l.addBreadcrumb(o)}async function fe(){await(l==null?void 0:l.flush())}function he(){l==null||l.disable()}function ge(){l==null||l.enable()}var ct={init:re,getClient:ie,captureException:se,captureMessage:oe,captureEvent:ae,setUser:ce,setTag:ue,setTags:le,setExtra:pe,addBreadcrumb:de,flush:fe,disable:he,enable:ge};export{V as BehaviorPlugin,K as Client,A as ErrorPlugin,z as NetworkPlugin,Q as OfflinePlugin,X as PerformancePlugin,de as addBreadcrumb,ae as captureEvent,se as captureException,oe as captureMessage,ct as default,he as disable,ge as enable,fe as flush,ie as getClient,re as init,pe as setExtra,ue as setTag,le as setTags,ce as setUser};
+function K(o){try{let e=o.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);if(!e)throw new Error("Invalid DSN format");let[,t,n,r,i]=e;return{protocol:t,publicKey:n,host:r,projectId:i}}catch(e){throw new Error(`Failed to parse DSN: ${e.message}`)}}function k(o){return`${o.protocol}://${o.host}/api/${o.projectId}/envelope/?sentry_key=${o.publicKey}`}function V(){return"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".replace(/[x]/g,()=>(Math.random()*16|0).toString(16))}function d(){return Date.now()}function y(o){return o!==null&&typeof o=="object"&&!Array.isArray(o)}function T(o,e){return!e||e.length===0?!1:e.some(t=>typeof t=="string"?o.includes(t):t.test(o))}function oe(o){let e=5381,t=o.length;for(;t;)e=e*33^o.charCodeAt(--t);return e>>>0}function D(o){return oe(o).toString(16)}function L(o,e,t=[]){let n=t.slice(0,3).map(i=>`${i.filename}:${i.lineno||0}`).join("|"),r=e.replace(/['"]?\d+['"]?/g,"").replace(/\s+/g," ").trim();return D(`${o}:${r}:${n}`)}function P(o,e={}){if(!o)return[];let{captureNodeModules:t=!1,captureColumn:n=!0,relativePathOnly:r=!1,maxFrames:i}=e,s=[],a=o.split(`
+`),c=typeof location!="undefined"?`${location.protocol}//${location.host}`:"";for(let p of a){let w=p.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(w){let[,f,h,v,g]=w,E=h.includes("node_modules");if(!t&&E)continue;let S=h;r&&c&&h.startsWith(c)&&(S=h.substring(c.length)),s.push({filename:S,function:f,lineno:parseInt(v,10),colno:n?parseInt(g,10):void 0,in_app:!E})}else{let f=p.match(/^\s*at\s*(.+?)@(.+?):(\d+):(\d+)\s*$/);if(f){let[,v,g,E,S]=f,R=g.includes("node_modules");if(!t&&R)continue;let A=g;r&&c&&g.startsWith(c)&&(A=g.substring(c.length)),s.push({filename:A,function:v||"",lineno:parseInt(E,10),colno:n?parseInt(S,10):void 0,in_app:!R});continue}let h=p.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(h){let[,v,g,E]=h,S=v.includes("node_modules");if(!t&&S)continue;let R=v;r&&c&&v.startsWith(c)&&(R=v.substring(c.length)),s.push({filename:R,lineno:parseInt(g,10),colno:n?parseInt(E,10):void 0,in_app:!S})}}}let u=s.reverse();return i!==void 0&&i>0?u.slice(0,i):u}function ae(o){let e=[{name:"WeChat",pattern:/MicroMessenger\/([\d.]+)/i},{name:"QQ Browser",pattern:/QIBrowser\/([\d.]+)/i},{name:"UC Browser",pattern:/UCBrowser\/([\d.]+)/i},{name:"Edge",pattern:/Edg\/([\d.]+)/i},{name:"Edge",pattern:/Edge\/([\d.]+)/i},{name:"Opera",pattern:/OPR\/([\d.]+)/i},{name:"Opera",pattern:/Opera\/([\d.]+)/i},{name:"Firefox",pattern:/Firefox\/([\d.]+)/i},{name:"Safari",pattern:/Version\/([\d.]+).*Safari/i},{name:"Chrome",pattern:/Chrome\/([\d.]+)/i},{name:"IE",pattern:/MSIE ([\d.]+)/i},{name:"IE",pattern:/Trident.*rv:([\d.]+)/i}];for(let t of e){let n=o.match(t.pattern);if(n){let r=n[1]||"",i=r.split(".")[0]||"";return{name:t.name,version:r,major:i}}}return{name:"Unknown",version:"",major:""}}function ce(o){let e=[{name:"Windows 11",pattern:/Windows NT 10\.0.*Win64.*x64/i},{name:"Windows 10",pattern:/Windows NT 10\.0/i},{name:"Windows 8.1",pattern:/Windows NT 6\.3/i},{name:"Windows 8",pattern:/Windows NT 6\.2/i},{name:"Windows 7",pattern:/Windows NT 6\.1/i},{name:"Windows Vista",pattern:/Windows NT 6\.0/i},{name:"Windows XP",pattern:/Windows NT 5\.1/i},{name:"macOS",pattern:/Mac OS X ([\d_.]+)/i},{name:"iOS",pattern:/OS ([\d_.]+) like Mac OS X/i},{name:"Android",pattern:/Android ([\d.]+)/i},{name:"Android",pattern:/Android/i},{name:"Linux",pattern:/Linux/i}];for(let t of e){let n=o.match(t.pattern);if(n){let r=n[1]||"";return r&&(r=r.replace(/_/g,".")),{name:t.name,version:r}}}return{name:"Unknown",version:""}}function ue(o){let e=/Mobile|Android|iPhone|iPod|BlackBerry|Windows Phone|Opera Mini/i.test(o),t=/Tablet|iPad|PlayBook|Kindle|Silk/i.test(o),n="desktop";t?n="tablet":e&&(n="mobile");let r="",i="",s=o.match(/iPhone/i),a=o.match(/iPad/i),c=o.match(/SM-([A-Z0-9]+)/i),u=o.match(/Pixel ([0-9A-Z]+)/i),p=o.match(/HUAWEI ([A-Z0-9]+)/i);return s?(r="Apple",i="iPhone"):a?(r="Apple",i="iPad"):c?(r="Samsung",i=c[1]):u?(r="Google",i=`Pixel ${u[1]}`):p&&(r="Huawei",i=p[1]),{type:n,vendor:r,model:i}}function z(){let o=typeof navigator!="undefined"?navigator.userAgent:"",e=ae(o),t=ce(o),n=ue(o);return{browser:e,os:t,device:n,url:typeof location!="undefined"?location.href:"",referrer:typeof document!="undefined"?document.referrer:"",title:typeof document!="undefined"?document.title:"",language:typeof navigator!="undefined"?navigator.language:"",user_agent:o,screen_width:typeof screen!="undefined"?screen.width:0,screen_height:typeof screen!="undefined"?screen.height:0,viewport_width:typeof window!="undefined"?window.innerWidth:0,viewport_height:typeof window!="undefined"?window.innerHeight:0}}function b(){return typeof location!="undefined"?location.href:""}function Q(){return typeof document!="undefined"?document.referrer:""}function x(o){if(!o)return o;try{let e=new URL(o),t=["password","passwd","pwd","token","access_token","refresh_token","api_key","apikey","key","secret","private_key","code","auth","id_token","idToken","session","session_id"],n=e.searchParams,r=!1;for(let i of Array.from(n.keys())){let s=i.toLowerCase().replace(/[-_]/g,"_");t.some(a=>s.includes(a))&&(n.set(i,"[Filtered]"),r=!0)}if(e.hash&&e.hash.length>1){let i=e.hash.substring(1);if(i.includes("=")){let s=new URLSearchParams(i),a=!1;for(let c of Array.from(s.keys())){let u=c.toLowerCase().replace(/[-_]/g,"_");t.some(p=>u.includes(p))&&(s.set(c,"[Filtered]"),a=!0)}a&&(e.hash="#"+s.toString(),r=!0)}}return r?e.toString():o}catch(e){return o}}function C(o,e=[]){return!e||e.length===0?!1:e.some(t=>{if(typeof t=="string"){if(t.includes("*")){let n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(n).test(o)}return o.includes(t)}return t.test(o)})}var le={Chrome:"c",Safari:"s",Firefox:"f",Edge:"e",Opera:"o",IE:"i",WeChat:"w","QQ Browser":"q","UC Browser":"u","Mobile Chrome":"cm","Mobile Safari":"sm"},de={"Windows 11":"w11","Windows 10":"w10","Windows 8.1":"w81","Windows 8":"w8","Windows 7":"w7","Windows Vista":"wv","Windows XP":"wxp",macOS:"m",iOS:"i",Android:"a",Linux:"l"};function F(){let o=z(),e=le[o.browser.name]||o.browser.name.toLowerCase(),t=de[o.os.name]||o.os.name.toLowerCase();return{browser:{name:e,version:o.browser.major},os:{name:t,version:o.os.version.split(".")[0]},device:{type:o.device.type,brand:o.device.vendor||void 0,model:o.device.model||void 0}}}function G(){let o=z(),e=`${o.browser.name}|${o.browser.major}|${o.os.name}|${o.os.version.split(".")[0]}|${o.device.type}|${o.device.vendor}|${o.device.model}|${o.language}`;return D(e).substring(0,16)}var pe={enabled:!0,sampleRate:1,maxQueueSize:100,flushInterval:5e3,maxRetries:3,retryDelay:1e3,environment:"production",contextLevel:{fatal:{maxStackFrames:5,maxBreadcrumbs:20},error:{maxStackFrames:5,maxBreadcrumbs:10},warning:{maxStackFrames:3,maxBreadcrumbs:5},info:{maxStackFrames:0,maxBreadcrumbs:0},debug:{maxStackFrames:0,maxBreadcrumbs:0}}},M=class{constructor(e){this.contexts=null;this.contextId=null;this.contextReported=!1;this.config={...pe,...e}}markContextReported(){this.contextReported=!0}get(e){return this.config[e]}getAll(){return{...this.config}}set(e,t){this.config[e]=t}setUser(e){this.config.user=e||void 0}setTags(e){this.config.tags={...this.config.tags||{},...e}}setTag(e,t){this.config.tags||(this.config.tags={}),this.config.tags[e]=t}setExtra(e,t){this.config.extra||(this.config.extra={}),this.config.extra[e]=t}isIgnoredError(e){return T(e,this.config.ignoreErrors||[])}getEncodedContext(){return this.contexts||(this.contexts=F()),this.contexts}applyToEvent(e,t=!0){var i,s,a,c;let n={...e};if(e.tags&&(n.tags={...e.tags}),e.request&&(n.request={...e.request}),"exception"in e&&e.exception){let u=e.exception;n.exception={...u,stacktrace:u.stacktrace?{...u.stacktrace,frames:u.stacktrace.frames?u.stacktrace.frames.map(p=>({...p})):void 0}:void 0}}e.breadcrumbs&&(n.breadcrumbs=e.breadcrumbs.map(u=>({...u}))),e.user&&(n.user={...e.user}),e.contexts&&(n.contexts={...e.contexts}),this.config.release&&(n.release=this.config.release),this.config.environment&&(n.environment=this.config.environment),this.config.user&&(n.user=this.config.user),this.config.tags&&(n.tags={...this.config.tags,...e.tags||{}}),n.request?n.request.url||(n.request.url=b()):n.request={url:b(),referrer:Q()},this.contextId||(this.contextId=G()),n.context_id=this.contextId;let r=this.applyContextLevel(n);return t&&!this.contextReported&&(this.contexts||(this.contexts=F()),r.contexts=this.contexts,r.env={b:(i=this.contexts.browser)==null?void 0:i.name,bv:(s=this.contexts.browser)==null?void 0:s.version,os:(a=this.contexts.os)==null?void 0:a.name,osv:(c=this.contexts.os)==null?void 0:c.version}),r}applyContextLevel(e){let t=this.config.contextLevel;if(!t)return e;let n=e.level||"error",r=t[n]||t.error,i={...e};if("exception"in e&&e.exception&&typeof e.exception=="object"){let s=e.exception;if(s.stacktrace&&typeof s.stacktrace=="object"){let a=s.stacktrace;Array.isArray(a.frames)&&(r.maxStackFrames===0?delete i.exception:i.exception={...s,stacktrace:{...a,frames:a.frames.slice(0,r.maxStackFrames)}})}}return e.breadcrumbs&&Array.isArray(e.breadcrumbs)&&(r.maxBreadcrumbs===0?delete i.breadcrumbs:i.breadcrumbs=e.breadcrumbs.slice(-r.maxBreadcrumbs)),i}};var B=class{constructor(){this.handlers=new Map}on(e,t){this.handlers.has(e)||this.handlers.set(e,[]),this.handlers.get(e).push(t)}off(e,t){let n=this.handlers.get(e);if(n){let r=n.indexOf(t);r>-1&&n.splice(r,1)}}emit(e,...t){let n=this.handlers.get(e);if(n)for(let r of n)try{r(...t)}catch(i){console.error("[LightSDK] EventBus handler error:",i)}}once(e,t){let n=(...r)=>{this.off(e,n),t(...r)};this.on(e,n)}destroy(){this.handlers.clear()}};var I=class{constructor(e,t,n,r,i){this.queue=[];this.timer=null;this.lastFlushTime=0;this.isFlushing=!1;this.dedupeMap=new Map;this.pendingCounts=new Map;this.errorRateWindow={};this.paused=!1;this.pauseTimer=null;this.maxSize=e,this.flushInterval=t,this.eventBus=n,this.flushCallback=r,this.syncFlushCallback=i}start(){this.startTimer(),this.setupVisibilityListener()}startTimer(){this.timer||(this.timer=setInterval(()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flush()},this.flushInterval))}setupVisibilityListener(){this.onVisibilityChange=()=>{document.hidden&&(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},typeof document!="undefined"&&document.addEventListener("visibilitychange",this.onVisibilityChange),this.onBeforeUnload=()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},this.onPagehide=()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},typeof window!="undefined"&&(window.addEventListener("beforeunload",this.onBeforeUnload),window.addEventListener("pagehide",this.onPagehide))}checkInfiniteLoop(e){let t=d(),n=t-1e3;this.errorRateWindow[e]||(this.errorRateWindow[e]=[]);let r=this.errorRateWindow[e];for(r.push(t);r.length>0&&r[0]10?(this.paused||(console.warn("[LightSDK] Infinite loop detected, pausing SDK for 60s",{fingerprint:e,count:r.length}),this.eventBus.emit("error",new Error("Infinite loop detected")),this.paused=!0,this.pauseTimer&&clearTimeout(this.pauseTimer),this.pauseTimer=setTimeout(()=>{this.paused=!1,this.errorRateWindow={},console.info("[LightSDK] Resumed after infinite loop detection")},6e4)),!0):!1}enqueue(e){if(this.paused)return;this.cleanupExpiredDedupeEntries();let t=this.getDedupeKey(e);if(t){if(this.checkInfiniteLoop(t))return;let n=d(),r=this.dedupeMap.get(t);if(r)if(n-r.lastTime<6e4){if(r.count>=3){if(!this.pendingCounts.has(t))this.pendingCounts.set(t,{count:1,ts_start:n,ts_end:n});else{let i=this.pendingCounts.get(t);i.count++,i.ts_end=n}return}r.count++,r.lastTime=n}else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!0}),this.pendingCounts.delete(t);else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!1})}this.queue.push(e),this.eventBus.emit("event",e),this.queue.length>=this.maxSize&&this.flush()}cleanupExpiredDedupeEntries(){if(this.dedupeMap.size<100&&Math.random()>.1)return;let e=d(),t=6e4;for(let[n,r]of this.dedupeMap)e-r.lastTime>t&&this.dedupeMap.delete(n)}buildCountEvents(){let e=[];for(let[t,n]of this.pendingCounts.entries()){let r={type:"count",fingerprint:t,count:n.count,ts_start:n.ts_start,ts_end:n.ts_end,timestamp:d(),level:"info"};e.push(r)}return e}getDedupeKey(e){if(e.type==="error"){let t=e;return t.fingerprint||t.message}return null}async flush(){if(this.queue.length===0&&this.pendingCounts.size===0||this.isFlushing)return;this.isFlushing=!0;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=d(),this.eventBus.emit("report",n);try{await this.flushCallback(n),this.pendingCounts.clear(),this.eventBus.emit("reported",n)}catch(r){throw e.forEach(i=>this.queue.unshift(i)),r}finally{this.isFlushing=!1}}flushSync(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=new Map(this.pendingCounts),n=this.buildCountEvents(),r=[...e,...n];this.lastFlushTime=d(),this.eventBus.emit("report",r);try{this.syncFlushCallback(r),this.pendingCounts.clear(),this.eventBus.emit("reported",r)}catch(i){console.error("[LightSDK] Sync flush failed",i),e.forEach(s=>this.queue.unshift(s));for(let[s,a]of t)this.pendingCounts.has(s)||this.pendingCounts.set(s,a)}}size(){return this.queue.length}destroy(){this.timer&&(clearInterval(this.timer),this.timer=null),this.pauseTimer&&(clearTimeout(this.pauseTimer),this.pauseTimer=null),typeof document!="undefined"&&this.onVisibilityChange&&document.removeEventListener("visibilitychange",this.onVisibilityChange),typeof window!="undefined"&&(this.onBeforeUnload&&window.removeEventListener("beforeunload",this.onBeforeUnload),this.onPagehide&&window.removeEventListener("pagehide",this.onPagehide)),this.dedupeMap.clear(),this.pendingCounts.clear(),this.errorRateWindow={}}};var J={event_id:"eid",timestamp:"ts",start_timestamp:"sts",type:"t",level:"l",message:"m",platform:"p",release:"r",environment:"e",fingerprint:"fp",context_id:"cid",user:"u",tags:"tg",exception:"ex",request:"req",contexts:"ctx",breadcrumbs:"bc",transaction:"tx",duration:"d",spans:"sp",extra:"xt"},qe=Object.fromEntries(Object.entries(J).map(([o,e])=>[e,o]));function Z(o){if(!o||typeof o!="object")return o;let e={};for(let[t,n]of Object.entries(o)){let r=J[t]||t;e[r]=n}return e}var _=class{constructor(e,t,n){this.useShortFields=!0;this.dsn=e,this.maxRetries=t,this.retryDelay=n}async report(e){if(e.length===0)return;let t=this.buildEnvelope(e),n=0;for(;n<=this.maxRetries;)try{await this.send(t,!1);return}catch(r){let i=r.status;if(i&&i>=400&&i<500||(n++,n>this.maxRetries))throw r;let s=Math.min(this.retryDelay*Math.pow(2,n-1),5e3);await this.delay(s)}}reportSync(e){if(e.length===0)return;let t=this.buildEnvelope(e);try{if(this.sendSync(t))return}catch(n){}this.sendViaImage(t)}buildEnvelope(e){let t=this.extractSharedMeta(e),n={event_id:V(),sent_at:new Date().toISOString(),meta:t};this.useShortFields&&(n._sf=1);let r=this.applyRelativeTimestamps(e,n),s=[JSON.stringify(n)];for(let a of r){let c=this.stripSharedFields(a,t);this.useShortFields&&(c=Z(c));let u=JSON.stringify(c),p=JSON.stringify({type:this.getEnvelopeType(a),length:u.length});s.push(p,u)}return s.join(`
+`)}applyRelativeTimestamps(e,t){if(e.length<=1)return e;let n=[],r=e[0].timestamp?new Date(e[0].timestamp).getTime():Date.now();t._rt=1,t._bt=r;for(let i=0;is.release===t.release)&&(n.release=t.release),t.environment&&e.every(s=>s.environment===t.environment)&&(n.environment=t.environment),t.user&&e.every(s=>JSON.stringify(s.user)===JSON.stringify(t.user))&&(n.user=t.user);let r=t.env;return r&&(n.env=r),n}stripSharedFields(e,t){let n={...e};return t.release&&n.release===t.release&&delete n.release,t.environment&&n.environment===t.environment&&delete n.environment,t.user&&JSON.stringify(n.user)===JSON.stringify(t.user)&&delete n.user,t.env&&delete n.env,n}getEnvelopeType(e){switch(e.type){case"error":return"event";case"performance":return"transaction";default:return e.type}}async send(e,t=!1){let n=k(this.dsn);if(navigator.sendBeacon)try{let r=new Blob([e],{type:"application/x-sentry-envelope"});if(navigator.sendBeacon(n,r))return}catch(r){}if(typeof fetch=="function")try{let r=new AbortController,i=setTimeout(()=>r.abort(),1e4),s=await fetch(n,{method:"POST",body:e,headers:{"Content-Type":"application/x-sentry-envelope"},keepalive:!0,signal:r.signal});if(clearTimeout(i),s.ok)return;let a=new Error(`HTTP ${s.status}`);throw a.status=s.status,a}catch(r){throw r}if(typeof XMLHttpRequest!="undefined")return new Promise((r,i)=>{let s=new XMLHttpRequest;s.open("POST",n,!t),s.setRequestHeader("Content-Type","application/x-sentry-envelope"),s.timeout=1e4,s.onload=()=>{if(s.status>=200&&s.status<300)r();else{let a=new Error(`HTTP ${s.status}`);a.status=s.status,i(a)}},s.onerror=()=>i(new Error("Network error")),s.ontimeout=()=>i(new Error("Request timeout")),s.send(e)});throw new Error("No transport available")}sendSync(e){let t=k(this.dsn);if(navigator.sendBeacon)try{let n=new Blob([e],{type:"application/x-sentry-envelope"});return navigator.sendBeacon(t,n)}catch(n){return!1}if(typeof XMLHttpRequest!="undefined")try{let n=new XMLHttpRequest;return n.open("POST",t,!1),n.setRequestHeader("Content-Type","application/x-sentry-envelope"),n.send(e),n.status>=200&&n.status<300}catch(n){return!1}return!1}sendViaImage(e){try{let t=k(this.dsn),n=new Image,r=encodeURIComponent(btoa(e)),i=t.includes("?")?"&":"?";return n.src=t+i+"sentry_data="+r.substring(0,2e3),!0}catch(t){return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};var O=class{constructor(e){this.plugins=new Map;this.client=e}add(e){if(!this.plugins.has(e.name)){this.plugins.set(e.name,e);try{e.setup(this.client)}catch(t){console.error(`[LightSDK] Plugin "${e.name}" setup error:`,t)}}}remove(e){let t=this.plugins.get(e);if(t){if(t.destroy)try{t.destroy()}catch(n){console.error(`[LightSDK] Plugin "${e}" destroy error:`,n)}this.plugins.delete(e)}}get(e){return this.plugins.get(e)}has(e){return this.plugins.has(e)}applyBeforeReport(e){let t=e;for(let n of this.plugins.values())if(n.beforeReport)try{let r=n.beforeReport(t);if(r===null)return null;t=r}catch(r){console.error(`[LightSDK] Plugin "${n.name}" beforeReport error:`,r)}return t}applyAfterReport(e){for(let t of this.plugins.values())if(t.afterReport)try{t.afterReport(e)}catch(n){console.error(`[LightSDK] Plugin "${t.name}" afterReport error:`,n)}}destroy(){for(let e of this.plugins.values())if(e.destroy)try{e.destroy()}catch(t){console.error(`[LightSDK] Plugin "${e.name}" destroy error:`,t)}this.plugins.clear()}};var U=class{constructor(e){this.breadcrumbs=[];this.enabled=!0;var t,n,r,i;if(!e.dsn)throw new Error("DSN is required");this.dsn=K(e.dsn),this.configManager=new M(e),this.eventBus=new B,this.reporter=new _(this.dsn,(t=this.configManager.get("maxRetries"))!=null?t:3,(n=this.configManager.get("retryDelay"))!=null?n:1e3),this.pluginManager=new O(this),this.queue=new I((r=this.configManager.get("maxQueueSize"))!=null?r:100,(i=this.configManager.get("flushInterval"))!=null?i:5e3,this.eventBus,async s=>this.flushEvents(s),s=>this.syncFlushEvents(s)),this.enabled=this.configManager.get("enabled")!==!1}get maxBreadcrumbs(){var e;return(e=this.configManager.get("maxBreadcrumbs"))!=null?e:20}get config(){return this.configManager.getAll()}init(){this.enabled&&(this.queue.start(),this.emit("ready"))}async flushEvents(e){let t=[];for(let i of e){let s=this.pluginManager.applyBeforeReport(i);s&&t.push(s)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let s=this.configManager.applyToEvent(i,n);n=!1;let a=this.configManager.get("beforeSend"),c=a?a(s):s;c&&r.push(c)}if(r.length!==0){await this.reporter.report(r),this.configManager.markContextReported();for(let i of r)this.pluginManager.applyAfterReport(i)}}syncFlushEvents(e){let t=[];for(let i of e){let s=this.pluginManager.applyBeforeReport(i);s&&t.push(s)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let s=this.configManager.applyToEvent(i,n);n=!1;let a=this.configManager.get("beforeSend"),c=a?a(s):s;c&&r.push(c)}r.length!==0&&(this.reporter.reportSync(r),this.configManager.markContextReported())}on(e,t){this.eventBus.on(e,t)}off(e,t){this.eventBus.off(e,t)}emit(e,...t){this.eventBus.emit(e,...t)}captureException(e){if(!this.enabled)return;let t=this.buildErrorEvent(e);this.configManager.isIgnoredError(t.message)||(t.breadcrumbs=[...this.breadcrumbs],this.queue.enqueue(t))}captureMessage(e,t="info"){if(!this.enabled)return;let n={type:"error",level:t,message:e,timestamp:d(),breadcrumbs:[...this.breadcrumbs]};this.configManager.isIgnoredError(e)||this.queue.enqueue(n)}captureEvent(e){if(!this.enabled)return;let t={timestamp:d(),level:"info",breadcrumbs:[...this.breadcrumbs],...e};this.queue.enqueue(t)}buildErrorEvent(e){let t=d();if(e instanceof Error){let r=P(e.stack,{maxFrames:5}),i=L(e.name,e.message,r);return{type:"error",level:"error",message:e.message,timestamp:t,exception:{type:e.name,value:e.message,stacktrace:{frames:r}},fingerprint:i}}return{type:"error",level:"error",message:String(e),timestamp:t}}setUser(e){this.configManager.setUser(e)}setTag(e,t){this.configManager.setTag(e,t)}setTags(e){this.configManager.setTags(e)}setExtra(e,t){this.configManager.setExtra(e,t)}addBreadcrumb(e){let t={...e,timestamp:d()};this.breadcrumbs.push(t),this.breadcrumbs.length>this.maxBreadcrumbs&&this.breadcrumbs.shift()}async flush(){await this.queue.flush()}disable(){this.enabled=!1}enable(){this.enabled=!0}use(e){this.pluginManager.add(e)}destroy(){this.queue.destroy(),this.pluginManager.destroy(),this.eventBus.destroy(),this.breadcrumbs=[]}},Y=U;var N=class{constructor(){this.name="error";this.version="1.0.0";this.config={maxStackFrames:5,captureNodeModules:!1,captureColumn:!0,relativePathOnly:!1,resourceSampleRate:.1}}setup(e){this.client=e,this.loadConfig(),this.setupGlobalError(),this.setupUnhandledRejection(),this.setupResourceError()}loadConfig(){let e=this.client.config;e&&y(e.error)&&(this.config={...this.config,...e.error}),e.ignoreErrors&&(this.config.ignoreErrors=[...this.config.ignoreErrors||[],...e.ignoreErrors]),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls]),e.includePaths&&(this.config.includePaths=[...this.config.includePaths||[],...e.includePaths])}shouldIgnoreError(e){return e?T(e,this.config.ignoreErrors||[]):!1}shouldIgnoreScriptUrl(e){return C(e,this.config.ignoreUrls||[])}shouldIncludePath(e){let t=this.config.includePaths||[];return t.length===0?!0:e.some(n=>t.some(r=>{var i;return typeof r=="string"?(i=n.filename)==null?void 0:i.includes(r):r.test(n.filename||"")}))}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:1;return e>=1?!0:e<=0?!1:Math.random()=1?!0:e<=0?!1:Math.random(){try{if(this.originalOnError&&this.originalOnError.call(window,e,t,n,r,i),t&&this.shouldIgnoreScriptUrl(t))return!1;let s=this.buildErrorEvent(i||e,t,n,r);s&&this.client.captureEvent(s)}catch(s){console.error("[LightSDK] ErrorPlugin global error handler error:",s)}return!1})}setupUnhandledRejection(){typeof window!="undefined"&&(this.onUnhandledRejection=e=>{try{let t=e.reason,n=this.extractUrlFromReason(t);if(n&&this.shouldIgnoreScriptUrl(n))return;let r=this.buildErrorEvent(t);r&&(r.type==="error"&&(r.tags={...r.tags||{},unhandled:"true",promise:"true"}),this.client.captureEvent(r))}catch(t){console.error("[LightSDK] ErrorPlugin unhandledrejection handler error:",t)}},window.addEventListener("unhandledrejection",this.onUnhandledRejection))}extractUrlFromReason(e){if(e instanceof Error&&e.stack){let t=e.stack.match(/at\s+(?:.+?\s+)?\((https?:\/\/[^:]+):\d+:\d+\)/);if(t)return t[1];let n=e.stack.match(/(https?:\/\/[^\s)]+):\d+:\d+/);if(n)return n[1]}return null}setupResourceError(){typeof window!="undefined"&&(this.onResourceError=e=>{var i;let t=e.target;if(!t)return;let n=(i=t.tagName)==null?void 0:i.toLowerCase(),r=t.src||t.href;if(n&&r&&["img","script","link","audio","video"].includes(n))try{if(this.shouldIgnoreScriptUrl(r)||!this.shouldSampleResource())return;let s=`resource:${n}`,a=x(r);this.client.captureEvent({type:"error",level:"warning",message:`Resource load failed: ${n} ${a}`,timestamp:d(),fingerprint:s,tags:{resource_type:n,resource_url:a},exception:{type:"ResourceError",value:`Failed to load ${n} resource`}})}catch(s){console.error("[LightSDK] ErrorPlugin resource error handler error:",s)}},window.addEventListener("error",this.onResourceError,!0))}buildErrorEvent(e,t,n,r){let i=d();if(e instanceof Error){if(this.shouldIgnoreError(e.message))return null;let c=P(e.stack,{captureNodeModules:this.config.captureNodeModules,captureColumn:this.config.captureColumn,relativePathOnly:this.config.relativePathOnly,maxFrames:this.config.maxStackFrames});if(!this.shouldIncludePath(c)||!this.shouldSample())return null;let u=L(e.name,e.message,c);return{type:"error",level:"error",message:e.message,timestamp:i,exception:{type:e.name,value:e.message,stacktrace:{frames:c}},fingerprint:u}}let s=String(e);if(this.shouldIgnoreError(s))return null;let a=[];return t&&a.push({filename:t,lineno:n,colno:this.config.captureColumn?r:void 0,in_app:!0}),!this.shouldIncludePath(a)||!this.shouldSample()?null:{type:"error",level:"error",message:s,timestamp:i,exception:{type:"Error",value:s,stacktrace:{frames:a}}}}destroy(){typeof window!="undefined"&&(this.originalOnError!==void 0&&(window.onerror=this.originalOnError),this.onUnhandledRejection&&window.removeEventListener("unhandledrejection",this.onUnhandledRejection),this.onResourceError&&window.removeEventListener("error",this.onResourceError,!0))}},ee=N;var q=class{constructor(){this.name="performance";this.version="1.0.0";this.config={sampleRate:.1,captureLongTasks:!0,captureResources:!1,resourceSampleRate:.01,longTaskSampleRate:.05,captureNavigation:!0,captureFP:!0};this.observers=[];this.navLoadTimer=null;this.totalBlockingTime=0;this.fcpTime=null;this.lastLongTaskEndTime=0;this.ttiReported=!1;this.ttiTimer=null}setup(e){this.client=e,this.loadConfig(),this.observeWebVitals(),this.observeLongTasks(),this.observeNavigation(),this.observeResources()}loadConfig(){let e=this.client.config;e&&y(e.performance)&&(this.config={...this.config,...e.performance})}shouldSample(e="default"){var n,r,i;let t=(n=this.config.sampleRate)!=null?n:.1;return e==="resource"?t=(r=this.config.resourceSampleRate)!=null?r:.01:e==="longtask"?t=(i=this.config.longTaskSampleRate)!=null?i:.05:e==="vitals"&&(t=1),t>=1?!0:t<=0?!1:Math.random(){let n=t.getEntriesByName("first-paint");n.length>0&&this.reportMetric("FP",n[0].startTime,"ms",{},"vitals")});e.observe({type:"paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeLCP(){try{let e=new PerformanceObserver(t=>{let n=t.getEntries(),r=n[n.length-1];if(r){let i=r.renderTime||r.loadTime||r.startTime;this.reportMetric("LCP",i,"ms",{},"vitals")}});e.observe({type:"largest-contentful-paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeFID(){try{let e=new PerformanceObserver(t=>{let n=t.getEntries();if(n.length>0){let r=n[0],i=r.processingStart-r.startTime;this.reportMetric("FID",i,"ms",{},"vitals")}});e.observe({type:"first-input",buffered:!0}),this.observers.push(e)}catch(e){}}observeCLS(){try{let e=0,t=0,n=[],r=new PerformanceObserver(i=>{let s=i.getEntries();for(let a of s)if(!a.hadRecentInput){let c=n[0],u=n[n.length-1];t&&a.startTime-((u==null?void 0:u.duration)||0)<1e3&&a.startTime-c.startTime<5e3?(t+=a.value||0,n.push(a)):(t=a.value||0,n=[a]),t>e&&(e=t,this.reportMetric("CLS",e,"",{},"vitals"))}});r.observe({type:"layout-shift",buffered:!0}),this.observers.push(r)}catch(e){}}observeFCP(){try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByName("first-contentful-paint");n.length>0&&(this.fcpTime=n[0].startTime,this.lastLongTaskEndTime=n[0].startTime,this.reportMetric("FCP",n[0].startTime,"ms",{},"vitals"),this.scheduleTTI())});e.observe({type:"paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeTTFB(){try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByType("navigation");if(n.length>0){let r=n[0],i=r.responseStart-r.requestStart;this.reportMetric("TTFB",Math.max(0,i),"ms",{},"vitals")}});e.observe({type:"navigation",buffered:!0}),this.observers.push(e)}catch(e){}}observeLongTasks(){if(this.config.captureLongTasks)try{let e=new PerformanceObserver(t=>{let n=t.getEntries();for(let r of n){let i=r.startTime+r.duration;if(i>this.lastLongTaskEndTime&&(this.lastLongTaskEndTime=i),this.fcpTime!==null&&r.startTime>=this.fcpTime){let s=r.duration-50;s>0&&(this.totalBlockingTime+=s)}this.reportMetric("longtask",r.duration,"ms",{},"longtask"),this.scheduleTTI()}});e.observe({type:"longtask",buffered:!0}),this.observers.push(e)}catch(e){}}scheduleTTI(){this.ttiReported||(this.ttiTimer&&clearTimeout(this.ttiTimer),this.ttiTimer=setTimeout(()=>{if(this.ttiReported)return;this.ttiReported=!0;let e=Math.max(this.lastLongTaskEndTime,this.fcpTime||0);this.reportMetric("TTI",e,"ms",{},"vitals"),this.reportMetric("TBT",this.totalBlockingTime,"ms",{},"vitals")},5e3))}observeNavigation(){this.config.captureNavigation&&typeof performance!="undefined"&&(this.onLoad=()=>{this.navLoadTimer=setTimeout(()=>{let e=performance.timing;if(!e)return;let t=e.navigationStart,n={dom_ready:e.domContentLoadedEventEnd-t,load_time:e.loadEventEnd-t,dns:e.domainLookupEnd-e.domainLookupStart,tcp:e.connectEnd-e.connectStart,ssl:e.secureConnectionStart>0?e.connectEnd-e.secureConnectionStart:0,ttfb:e.responseStart-e.requestStart,download:e.responseEnd-e.responseStart,dom_parse:e.domInteractive-e.responseEnd};for(let[r,i]of Object.entries(n))i>0&&this.reportMetric(r,i,"ms");this.navLoadTimer=null},0)},window.addEventListener("load",this.onLoad))}observeResources(){if(this.config.captureResources&&typeof performance!="undefined")try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByType("resource");for(let r of n)r.duration>1e3&&this.reportMetric("resource_slow",r.duration,"ms",{resource_name:r.name.substring(0,200),resource_type:r.initiatorType},"resource")});e.observe({type:"resource",buffered:!0}),this.observers.push(e)}catch(e){}}getRating(e,t){let r={LCP:{good:2500,poor:4e3},FCP:{good:1800,poor:3e3},FP:{good:1800,poor:3e3},FID:{good:100,poor:300},CLS:{good:.1,poor:.25},TTFB:{good:800,poor:1800}}[e];return!r||t<=r.good?"good":t<=r.poor?"needs-improvement":"poor"}reportMetric(e,t,n,r={},i="default"){if(!this.shouldSample(i))return;let s=this.getRating(e,t),a={type:"performance",level:"info",metric:e,value:Math.round(t*100)/100,unit:n,rating:s,timestamp:d(),tags:{metric:e,rating:s,...r}};this.client.captureEvent(a)}destroy(){for(let e of this.observers)try{e.disconnect()}catch(t){}this.observers=[],this.ttiTimer&&(clearTimeout(this.ttiTimer),this.ttiTimer=null),this.navLoadTimer&&(clearTimeout(this.navLoadTimer),this.navLoadTimer=null),typeof window!="undefined"&&this.onLoad&&window.removeEventListener("load",this.onLoad)}},te=q;var H=class{constructor(){this.name="network";this.version="1.0.0";this.config={captureXHR:!0,captureFetch:!0,captureSuccess:!1,captureBody:!1,captureRequestBody:!1,captureResponseBody:!1,captureRequestHeaders:["content-type","user-agent"],captureResponseHeaders:["content-type","content-length"],errorSampleRate:1,successSampleRate:.01,slowThreshold:3e3,slowSampleRate:1,ignoreStatusCodes:[]}}setup(e){this.client=e,this.loadConfig(),this.config.captureFetch&&this.patchFetch(),this.config.captureXHR&&this.patchXHR()}loadConfig(){let e=this.client.config;e&&y(e.network)&&(this.config={...this.config,...e.network}),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnore(e){return C(e,this.config.ignoreUrls||[])}shouldSample(e,t){var r,i,s,a;let n=e?(r=this.config.errorSampleRate)!=null?r:1:(i=this.config.successSampleRate)!=null?i:.01;return!e&&t>=((s=this.config.slowThreshold)!=null?s:3e3)&&(n=(a=this.config.slowSampleRate)!=null?a:1),n>=1?!0:n<=0?!1:Math.random(){var h;let p=d()-r,w=u.ok,f=u.status;return(h=e.config.ignoreStatusCodes)!=null&&h.includes(f)||!e.shouldSample(!w,p)||(!w||e.config.captureSuccess)&&e.reportNetwork({sub_type:"fetch",method:s.toUpperCase(),url:a,status_code:f,duration:p,success:w,error:w?void 0:`HTTP ${f}`}),u},u=>{let p=d()-r;throw e.shouldSample(!0,p)&&e.reportNetwork({sub_type:"fetch",method:s.toUpperCase(),url:a,duration:p,success:!1,error:(u==null?void 0:u.message)||"Network error"}),u}),c}}patchXHR(){if(typeof XMLHttpRequest=="undefined")return;this.originalXHROpen=XMLHttpRequest.prototype.open,this.originalXHRSend=XMLHttpRequest.prototype.send;let e=this;XMLHttpRequest.prototype.open=function(t,n){return this._lightMethod=t,this._lightUrl=typeof n=="string"?n:n.toString(),this._lightTracked=!1,e.originalXHROpen.apply(this,arguments)},XMLHttpRequest.prototype.send=function(t){let n=d(),r=this._lightMethod||"GET",i=this._lightUrl||"",s=this._lightTracked;if(e.shouldIgnore(i)||s)return e.originalXHRSend.apply(this,arguments);this._lightTracked=!0;let a=x(i),c=0;t&&typeof t=="string"&&(c=t.length);let u=this.onloadend;return this.onloadend=function(p){var v;let w=d()-n,f=this.status,h=f>=200&&f<300;if(!((v=e.config.ignoreStatusCodes)!=null&&v.includes(f))&&e.shouldSample(!h,w)&&(!h||e.config.captureSuccess)){let g=0;try{let E=this.getResponseHeader("content-length");E&&(g=parseInt(E,10)),!g&&this.responseText&&(g=this.responseText.length)}catch(E){}e.reportNetwork({sub_type:"xhr",method:r.toUpperCase(),url:a,status_code:f,duration:w,request_size:c||void 0,response_size:g||void 0,success:h,error:h?void 0:`HTTP ${f}`})}if(u)return u.call(this,p)},e.originalXHRSend.apply(this,arguments)}}reportNetwork(e){let t={type:"network",level:e.success?"info":"error",timestamp:d(),tags:{method:e.method,sub_type:e.sub_type,success:String(e.success)},...e};this.client.captureEvent(t)}destroy(){this.originalFetch&&(window.fetch=this.originalFetch),this.originalXHROpen&&(XMLHttpRequest.prototype.open=this.originalXHROpen),this.originalXHRSend&&(XMLHttpRequest.prototype.send=this.originalXHRSend)}},ne=H;var j=class{constructor(){this.name="behavior";this.version="1.0.0";this.config={capturePV:!0,captureClick:!0,captureRoute:!0,captureDuration:!0,captureScroll:!0,clickThrottle:300,scrollThrottle:1e3,sampleRate:.01};this.lastClickTime=0;this.lastScrollTime=0;this.maxScrollDepth=0;this.pageEnterTime=0;this.scrollReported=!1}setup(e){this.client=e,this.loadConfig(),this.config.capturePV&&this.trackPV(),this.config.captureClick&&this.trackClick(),this.config.captureRoute&&this.trackRoute(),this.config.captureDuration&&this.trackPageDuration(),this.config.captureScroll&&this.trackScroll()}loadConfig(){let e=this.client.config;e&&y(e.behavior)&&(this.config={...this.config,...e.behavior})}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:.01;return e>=1?!0:e<=0?!1:Math.random(){var a,c,u;let t=d(),n=(a=this.config.clickThrottle)!=null?a:300;if(t-this.lastClickTime{let r=b();if(r!==e){let i=e,s=r;e=r,this.reportBehavior({sub_type:"route",page_url:s,referrer:i,properties:{from:i,to:s}}),this.pageEnterTime=d(),this.maxScrollDepth=0,this.scrollReported=!1}};this.originalPushState=history.pushState,this.originalReplaceState=history.replaceState;let n=this;history.pushState=function(){let r=n.originalPushState.apply(this,arguments);return setTimeout(t,0),r},history.replaceState=function(){let r=n.originalReplaceState.apply(this,arguments);return setTimeout(t,0),r},this.onPopState=()=>{setTimeout(t,0)},this.onHashchange=()=>{setTimeout(t,0)},window.addEventListener("popstate",this.onPopState),window.addEventListener("hashchange",this.onHashchange)}trackPageDuration(){if(typeof window=="undefined")return;this.pageEnterTime=d();let e=()=>{let t=d()-this.pageEnterTime;t>1e3&&(this.config.captureScroll&&!this.scrollReported&&this.reportScroll(),this.reportBehavior({sub_type:"duration",page_url:b(),properties:{duration:t,max_scroll_depth:this.maxScrollDepth}}))};this.onVisibilityChange=()=>{document.hidden?e():this.pageEnterTime=d()},this.onBeforeUnload=e,this.onPagehide=e,document.addEventListener("visibilitychange",this.onVisibilityChange),window.addEventListener("beforeunload",this.onBeforeUnload),window.addEventListener("pagehide",this.onPagehide)}trackScroll(){typeof window!="undefined"&&(this.onScroll=()=>{var r;let e=d(),t=(r=this.config.scrollThrottle)!=null?r:1e3;if(e-this.lastScrollTimethis.maxScrollDepth&&(this.maxScrollDepth=n)},window.addEventListener("scroll",this.onScroll,{passive:!0}))}reportScroll(){this.scrollReported||(this.scrollReported=!0,this.reportBehavior({sub_type:"scroll",page_url:b(),properties:{max_depth:this.maxScrollDepth}}))}reportBehavior(e){if(!this.shouldSample())return;let t={type:"behavior",level:"info",timestamp:d(),tags:{sub_type:e.sub_type,page_url:b()},...e};this.client.captureEvent(t)}destroy(){typeof window!="undefined"&&(this.originalPushState&&(history.pushState=this.originalPushState),this.originalReplaceState&&(history.replaceState=this.originalReplaceState),this.onPopState&&window.removeEventListener("popstate",this.onPopState),this.onHashchange&&window.removeEventListener("hashchange",this.onHashchange),this.onBeforeUnload&&window.removeEventListener("beforeunload",this.onBeforeUnload),this.onPagehide&&window.removeEventListener("pagehide",this.onPagehide),this.onScroll&&window.removeEventListener("scroll",this.onScroll)),typeof document!="undefined"&&(this.onClick&&document.removeEventListener("click",this.onClick,!0),this.onVisibilityChange&&document.removeEventListener("visibilitychange",this.onVisibilityChange))}},re=j;var he="light-sentry-offline",m="events",$=1e3,W=class{constructor(){this.name="offline";this.version="1.0.0";this.config={maxEvents:$};this.db=null;this.isOnline=!0;this.isSyncing=!1;this.pendingEvents=[]}setup(e){this.client=e,this.loadConfig(),this.initDatabase(),this.setupNetworkListeners()}loadConfig(){let e=this.client.config;e&&typeof e.offline=="object"&&(this.config={...this.config,...e.offline})}async initDatabase(){if(typeof indexedDB=="undefined"){console.warn("[LightSDK] OfflinePlugin: IndexedDB not available");return}return new Promise((e,t)=>{let n=indexedDB.open(he,1);n.onerror=()=>{console.warn("[LightSDK] OfflinePlugin: Failed to open IndexedDB"),t(n.error)},n.onsuccess=()=>{this.db=n.result,e()},n.onupgradeneeded=r=>{let i=r.target.result,s=r.oldVersion;i.objectStoreNames.contains(m)||i.createObjectStore(m,{keyPath:"id"}).createIndex("timestamp","timestamp",{unique:!1})}})}setupNetworkListeners(){typeof window!="undefined"&&(this.isOnline=navigator.onLine,this.onOnline=()=>{console.info("[LightSDK] OfflinePlugin: Network online"),this.isOnline=!0,this.syncPendingEvents()},this.onOffline=()=>{console.info("[LightSDK] OfflinePlugin: Network offline"),this.isOnline=!1},window.addEventListener("online",this.onOnline),window.addEventListener("offline",this.onOffline))}generateId(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}async saveToIndexedDB(e){if(!this.db)return;let t=this.config.maxEvents||$,n=await this.getEventCount();return n>=t&&await this.deleteOldestEvents(n-t+1),new Promise((r,i)=>{let a=this.db.transaction([m],"readwrite").objectStore(m),c={id:this.generateId(),event:{...e,offline:!0},timestamp:d()},u=a.add(c);u.onsuccess=()=>r(),u.onerror=()=>i(u.error)})}getEventCount(){return this.db?new Promise((e,t)=>{let i=this.db.transaction([m],"readonly").objectStore(m).count();i.onsuccess=()=>e(i.result),i.onerror=()=>t(i.error)}):Promise.resolve(0)}async deleteOldestEvents(e){if(!(!this.db||e<=0))return new Promise((t,n)=>{let a=this.db.transaction([m],"readwrite").objectStore(m).index("timestamp").openCursor(),c=0;a.onsuccess=u=>{let p=u.target.result;p&&cn(a.error)})}async getAllStoredEvents(){return this.db?new Promise((e,t)=>{let s=this.db.transaction([m],"readonly").objectStore(m).index("timestamp").getAll();s.onsuccess=()=>{let a=s.result||[];a.sort((c,u)=>c.timestamp-u.timestamp),e(a)},s.onerror=()=>t(s.error)}):[]}async clearStoredEvents(){if(this.db)return new Promise((e,t)=>{let i=this.db.transaction([m],"readwrite").objectStore(m).clear();i.onsuccess=()=>e(),i.onerror=()=>t(i.error)})}async syncPendingEvents(){if(!(!this.isOnline||this.isSyncing)){this.isSyncing=!0,console.info("[LightSDK] OfflinePlugin: Syncing pending events");try{let e=0;for(;this.pendingEvents.length>0;){let n=this.pendingEvents.shift();try{this.client.captureEvent(n),e++}catch(r){console.error("[LightSDK] OfflinePlugin: Failed to re-enqueue memory event",r)}}let t=await this.getAllStoredEvents();if(t.length>0)for(let n of t)try{let r=n.event;this.client.captureEvent(r),e++}catch(r){console.error("[LightSDK] OfflinePlugin: Failed to re-enqueue event",r)}try{await this.client.flush()}catch(n){}await this.clearStoredEvents(),console.info(`[LightSDK] OfflinePlugin: Synced ${e} events`)}catch(e){console.error("[LightSDK] OfflinePlugin: Sync failed",e)}finally{this.isSyncing=!1}}}beforeReport(e){if(!this.isOnline){let n=this.config.maxEvents||$;return this.pendingEvents.length>=n&&this.pendingEvents.shift(),this.pendingEvents.push(e),this.savePendingEventsToDB(),null}let t={...e};return delete t.offline,t}savePendingEventsToDB(){if(this.pendingEvents.length===0)return;let e=this.pendingEvents.splice(0,this.pendingEvents.length),t=0,n=()=>{if(t>=e.length)return;let r=e[t];this.saveToIndexedDB(r).then(()=>{t++,n()}).catch(i=>{console.error("[LightSDK] OfflinePlugin: Failed to save event offline",i),this.pendingEvents.unshift(...e.slice(t))})};n()}destroy(){typeof window!="undefined"&&(this.onOnline&&window.removeEventListener("online",this.onOnline),this.onOffline&&window.removeEventListener("offline",this.onOffline)),this.db&&(this.db.close(),this.db=null)}},ie=W;var X=class{constructor(){this.name="sampling";this.version="1.0.0";this.config={rates:{error:1,performance:1,network:1,behavior:1}}}setup(e){this.client=e,this.loadConfig()}loadConfig(){let e=this.client.config;if(e&&y(e.sampling)){let t=e.sampling;"rates"in t&&y(t.rates)&&(this.config.rates={...this.config.rates,...t.rates})}}shouldSample(e){var r;let t=(r=this.client.config.sampleRate)!=null?r:1;if(t<=0)return!1;let n=this.getTypeRate(e);return t>=1?this.randomCheck(n):this.randomCheck(t*n)}getTypeRate(e){let t=this.config.rates||{},n=e.type;if(n==="count")return 1;let r=t[n];return r!==void 0?r:1}randomCheck(e){return e>=1?!0:e<=0?!1:Math.random()typeof i=="string"))||[];(n.includes("performance")||n.includes("all"))&&t.push(new te),(n.includes("network")||n.includes("all"))&&t.push(new ne),(n.includes("behavior")||n.includes("all"))&&t.push(new re);for(let i of t)e.use(i);if(o.plugins)for(let i of o.plugins)typeof i=="object"&&i!==null&&"name"in i&&"setup"in i&&e.use(i);return e.init(),l=e,e}function ge(){return l}function me(o){l==null||l.captureException(o)}function ve(o,e){l==null||l.captureMessage(o,e)}function ye(o){l==null||l.captureEvent(o)}function be(o){l==null||l.setUser(o)}function we(o,e){l==null||l.setTag(o,e)}function Ee(o){l==null||l.setTags(o)}function Se(o,e){l==null||l.setExtra(o,e)}function xe(o){l==null||l.addBreadcrumb(o)}async function Re(){await(l==null?void 0:l.flush())}function ke(){l==null||l.disable()}function Te(){l==null||l.enable()}var Pt={init:fe,getClient:ge,captureException:me,captureMessage:ve,captureEvent:ye,setUser:be,setTag:we,setTags:Ee,setExtra:Se,addBreadcrumb:xe,flush:Re,disable:ke,enable:Te};export{re as BehaviorPlugin,Y as Client,ee as ErrorPlugin,ne as NetworkPlugin,ie as OfflinePlugin,te as PerformancePlugin,se as SamplingPlugin,xe as addBreadcrumb,ye as captureEvent,me as captureException,ve as captureMessage,Pt as default,ke as disable,Te as enable,Re as flush,ge as getClient,fe as init,Se as setExtra,we as setTag,Ee as setTags,be as setUser};
diff --git a/dist/index.iife.js b/dist/index.iife.js
index 3f67a65..4740687 100644
--- a/dist/index.iife.js
+++ b/dist/index.iife.js
@@ -1,4 +1,3 @@
-"use strict";var LightSDK=(()=>{var T=Object.defineProperty;var le=Object.getOwnPropertyDescriptor;var pe=Object.getOwnPropertyNames;var de=Object.prototype.hasOwnProperty;var fe=(s,e)=>{for(var t in e)T(s,t,{get:e[t],enumerable:!0})},he=(s,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of pe(e))!de.call(s,r)&&r!==t&&T(s,r,{get:()=>e[r],enumerable:!(n=le(e,r))||n.enumerable});return s};var ge=s=>he(T({},"__esModule",{value:!0}),s);var Re={};fe(Re,{BehaviorPlugin:()=>$,Client:()=>B,ErrorPlugin:()=>O,NetworkPlugin:()=>U,OfflinePlugin:()=>K,PerformancePlugin:()=>F,addBreadcrumb:()=>oe,captureEvent:()=>te,captureException:()=>Y,captureMessage:()=>ee,default:()=>xe,disable:()=>ce,enable:()=>ue,flush:()=>ae,getClient:()=>Z,init:()=>J,setExtra:()=>se,setTag:()=>re,setTags:()=>ie,setUser:()=>ne});function A(s){try{let e=s.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);if(!e)throw new Error("Invalid DSN format");let[,t,n,r,i]=e;return{protocol:t,publicKey:n,host:r,projectId:i}}catch(e){throw new Error(`Failed to parse DSN: ${e.message}`)}}function b(s){return`${s.protocol}://${s.host}/${s.projectId}/envelope/`}function p(){return Date.now()}function v(s){return s!==null&&typeof s=="object"&&!Array.isArray(s)}function C(s){let e=0;for(let t=0;t`${i.filename}:${i.lineno||0}`).join("|"),r=e.replace(/['"]?\d+['"]?/g,"").replace(/\s+/g," ").trim();return C(`${s}:${r}:${n}`)}function me(s){let e=[{name:"WeChat",pattern:/MicroMessenger\/([\d.]+)/i},{name:"QQ Browser",pattern:/QIBrowser\/([\d.]+)/i},{name:"UC Browser",pattern:/UCBrowser\/([\d.]+)/i},{name:"Edge",pattern:/Edg\/([\d.]+)/i},{name:"Edge",pattern:/Edge\/([\d.]+)/i},{name:"Opera",pattern:/OPR\/([\d.]+)/i},{name:"Opera",pattern:/Opera\/([\d.]+)/i},{name:"Firefox",pattern:/Firefox\/([\d.]+)/i},{name:"Safari",pattern:/Version\/([\d.]+).*Safari/i},{name:"Chrome",pattern:/Chrome\/([\d.]+)/i},{name:"IE",pattern:/MSIE ([\d.]+)/i},{name:"IE",pattern:/Trident.*rv:([\d.]+)/i}];for(let t of e){let n=s.match(t.pattern);if(n){let r=n[1]||"",i=r.split(".")[0]||"";return{name:t.name,version:r,major:i}}}return{name:"Unknown",version:"",major:""}}function ve(s){let e=[{name:"Windows 11",pattern:/Windows NT 10\.0.*Win64.*x64/i},{name:"Windows 10",pattern:/Windows NT 10\.0/i},{name:"Windows 8.1",pattern:/Windows NT 6\.3/i},{name:"Windows 8",pattern:/Windows NT 6\.2/i},{name:"Windows 7",pattern:/Windows NT 6\.1/i},{name:"Windows Vista",pattern:/Windows NT 6\.0/i},{name:"Windows XP",pattern:/Windows NT 5\.1/i},{name:"macOS",pattern:/Mac OS X ([\d_.]+)/i},{name:"iOS",pattern:/OS ([\d_.]+) like Mac OS X/i},{name:"Android",pattern:/Android ([\d.]+)/i},{name:"Android",pattern:/Android/i},{name:"Linux",pattern:/Linux/i}];for(let t of e){let n=s.match(t.pattern);if(n){let r=n[1]||"";return r&&(r=r.replace(/_/g,".")),{name:t.name,version:r}}}return{name:"Unknown",version:""}}function ye(s){let e=/Mobile|Android|iPhone|iPod|BlackBerry|Windows Phone|Opera Mini/i.test(s),t=/Tablet|iPad|PlayBook|Kindle|Silk/i.test(s),n="desktop";t?n="tablet":e&&(n="mobile");let r="",i="",o=s.match(/iPhone/i),a=s.match(/iPad/i),c=s.match(/SM-([A-Z0-9]+)/i),u=s.match(/Pixel ([0-9A-Z]+)/i),d=s.match(/HUAWEI ([A-Z0-9]+)/i);return o?(r="Apple",i="iPhone"):a?(r="Apple",i="iPad"):c?(r="Samsung",i=c[1]):u?(r="Google",i=`Pixel ${u[1]}`):d&&(r="Huawei",i=d[1]),{type:n,vendor:r,model:i}}function X(){let s=typeof navigator!="undefined"?navigator.userAgent:"",e=me(s),t=ve(s),n=ye(s);return{browser:e,os:t,device:n,url:typeof location!="undefined"?location.href:"",referrer:typeof document!="undefined"?document.referrer:"",title:typeof document!="undefined"?document.title:"",language:typeof navigator!="undefined"?navigator.language:"",user_agent:s,screen_width:typeof screen!="undefined"?screen.width:0,screen_height:typeof screen!="undefined"?screen.height:0,viewport_width:typeof window!="undefined"?window.innerWidth:0,viewport_height:typeof window!="undefined"?window.innerHeight:0}}function g(){return typeof location!="undefined"?location.href:""}function z(){return typeof document!="undefined"?document.referrer:""}function M(s){if(!s)return s;try{let e=new URL(s),t=["password","passwd","pwd","token","access_token","refresh_token","api_key","apikey","key","secret","private_key","code","auth","id_token","idToken","session","session_id"],n=e.searchParams,r=!1;for(let i of Array.from(n.keys())){let o=i.toLowerCase().replace(/[-_]/g,"_");t.some(a=>o.includes(a))&&(n.set(i,"[Filtered]"),r=!0)}return r?e.toString():s}catch(e){return s}}function w(s,e=[]){return!e||e.length===0?!1:e.some(t=>{if(typeof t=="string"){if(t.includes("*")){let n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(n).test(s)}return s.includes(t)}return t.test(s)})}var be={Chrome:"c",Safari:"s",Firefox:"f",Edge:"e",Opera:"o",IE:"i",WeChat:"w","QQ Browser":"q","UC Browser":"u","Mobile Chrome":"cm","Mobile Safari":"sm"},Ee={"Windows 11":"w11","Windows 10":"w10","Windows 8.1":"w81","Windows 8":"w8","Windows 7":"w7","Windows Vista":"wv","Windows XP":"wxp",macOS:"m",iOS:"i",Android:"a",Linux:"l"};function I(){let s=X(),e=be[s.browser.name]||s.browser.name.toLowerCase(),t=Ee[s.os.name]||s.os.name.toLowerCase();return{browser:{name:e,version:s.browser.major},os:{name:t,version:s.os.version.split(".")[0]},device:{type:s.device.type,brand:s.device.vendor||void 0,model:s.device.model||void 0}}}function V(){let s=X(),e=`${s.browser.name}|${s.browser.major}|${s.os.name}|${s.os.version.split(".")[0]}|${s.device.type}|${s.device.vendor}|${s.device.model}|${s.language}`;return C(e).substring(0,16)}var we={enabled:!0,sampleRate:1,maxQueueSize:100,flushInterval:5e3,maxRetries:3,retryDelay:1e3,environment:"production",contextLevel:{fatal:{maxStackFrames:5,maxBreadcrumbs:20},error:{maxStackFrames:5,maxBreadcrumbs:10},warning:{maxStackFrames:3,maxBreadcrumbs:5},info:{maxStackFrames:0,maxBreadcrumbs:0},debug:{maxStackFrames:0,maxBreadcrumbs:0}}},S=class{constructor(e){this.contexts=null;this.contextId=null;this.contextReported=!1;this.config={...we,...e}}markContextReported(){this.contextReported=!0}get(e){return this.config[e]}getAll(){return{...this.config}}set(e,t){this.config[e]=t}setUser(e){this.config.user=e||void 0}setTags(e){this.config.tags={...this.config.tags||{},...e}}setTag(e,t){this.config.tags||(this.config.tags={}),this.config.tags[e]=t}setExtra(e,t){this.config.extra||(this.config.extra={}),this.config.extra[e]=t}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:1;return e>=1?!0:e<=0?!1:Math.random()typeof n=="string"?e.includes(n):n.test(e))}getEncodedContext(){return this.contexts||(this.contexts=I()),this.contexts}applyToEvent(e,t=!0){var n,r,i,o;return this.config.release&&(e.release=this.config.release),this.config.environment&&(e.environment=this.config.environment),this.config.user&&(e.user=this.config.user),this.config.tags&&(e.tags={...this.config.tags,...e.tags||{}}),e.request?e.request.url||(e.request.url=g()):e.request={url:g(),referrer:z()},this.contextId||(this.contextId=V()),e.context_id=this.contextId,this.applyContextLevel(e),t&&!this.contextReported&&(this.contexts||(this.contexts=I()),e.contexts=this.contexts,e.env={b:(n=this.contexts.browser)==null?void 0:n.name,bv:(r=this.contexts.browser)==null?void 0:r.version,os:(i=this.contexts.os)==null?void 0:i.name,osv:(o=this.contexts.os)==null?void 0:o.version}),e}applyContextLevel(e){var i,o,a,c;let t=this.config.contextLevel;if(!t)return;let n=e.level||"error",r=t[n]||t.error;(o=(i=e.exception)==null?void 0:i.stacktrace)!=null&&o.frames&&r.maxStackFrames>0?e.exception.stacktrace.frames=e.exception.stacktrace.frames.slice(0,r.maxStackFrames):r.maxStackFrames===0&&((c=(a=e.exception)==null?void 0:a.stacktrace)!=null&&c.frames)&&delete e.exception.stacktrace,e.breadcrumbs&&r.maxBreadcrumbs>0?e.breadcrumbs=e.breadcrumbs.slice(-r.maxBreadcrumbs):r.maxBreadcrumbs===0&&delete e.breadcrumbs}};var x=class{constructor(){this.handlers=new Map}on(e,t){this.handlers.has(e)||this.handlers.set(e,[]),this.handlers.get(e).push(t)}off(e,t){let n=this.handlers.get(e);if(n){let r=n.indexOf(t);r>-1&&n.splice(r,1)}}emit(e,...t){let n=this.handlers.get(e);if(n)for(let r of n)try{r(...t)}catch(i){console.error("[LightSDK] EventBus handler error:",i)}}once(e,t){let n=(...r)=>{this.off(e,n),t(...r)};this.on(e,n)}destroy(){this.handlers.clear()}};var R=class{constructor(e,t,n,r,i){this.queue=[];this.timer=null;this.lastFlushTime=0;this.dedupeMap=new Map;this.pendingCounts=new Map;this.errorRateWindow={};this.paused=!1;this.pauseTimer=null;this.maxSize=e,this.flushInterval=t,this.eventBus=n,this.flushCallback=r,this.syncFlushCallback=i}start(){this.startTimer(),this.setupVisibilityListener()}startTimer(){this.timer||(this.timer=setInterval(()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flush()},this.flushInterval))}setupVisibilityListener(){typeof document!="undefined"&&document.addEventListener("visibilitychange",()=>{document.hidden&&(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}),typeof window!="undefined"&&(window.addEventListener("beforeunload",()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}),window.addEventListener("pagehide",()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()}))}checkInfiniteLoop(e){let t=p(),n=t-1e3;this.errorRateWindow[e]||(this.errorRateWindow[e]=[]);let r=this.errorRateWindow[e];for(r.push(t);r.length>0&&r[0]10?(this.paused||(console.warn("[LightSDK] Infinite loop detected, pausing SDK for 60s",{fingerprint:e,count:r.length}),this.eventBus.emit("error",new Error("Infinite loop detected")),this.paused=!0,this.pauseTimer&&clearTimeout(this.pauseTimer),this.pauseTimer=setTimeout(()=>{this.paused=!1,this.errorRateWindow={},console.info("[LightSDK] Resumed after infinite loop detection")},6e4)),!0):!1}enqueue(e){if(this.paused)return;let t=this.getDedupeKey(e);if(t){if(this.checkInfiniteLoop(t))return;let n=p(),r=this.dedupeMap.get(t);if(r)if(n-r.lastTime<6e4){if(r.count>=3){if(!this.pendingCounts.has(t))this.pendingCounts.set(t,{count:1,ts_start:n,ts_end:n});else{let i=this.pendingCounts.get(t);i.count++,i.ts_end=n}return}r.count++,r.lastTime=n}else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!0}),this.pendingCounts.delete(t);else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!1})}this.queue.length>=this.maxSize&&this.flush(),this.queue.push(e),this.eventBus.emit("event",e),this.queue.length>=this.maxSize&&this.flush()}buildCountEvents(){let e=[];for(let[t,n]of this.pendingCounts.entries()){let r={type:"count",fingerprint:t,count:n.count,ts_start:n.ts_start,ts_end:n.ts_end,timestamp:p(),level:"info"};e.push(r)}return this.pendingCounts.clear(),e}getDedupeKey(e){if(e.type==="error"){let t=e;return t.fingerprint||t.message}return null}async flush(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=p(),this.eventBus.emit("report",n);try{await this.flushCallback(n),this.eventBus.emit("reported",n)}catch(r){throw e.forEach(i=>this.queue.unshift(i)),r}}flushSync(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=p(),this.eventBus.emit("report",n);try{this.syncFlushCallback(n),this.eventBus.emit("reported",n)}catch(r){console.error("[LightSDK] Sync flush failed",r)}}size(){return this.queue.length}destroy(){this.timer&&(clearInterval(this.timer),this.timer=null),this.pauseTimer&&(clearTimeout(this.pauseTimer),this.pauseTimer=null),this.dedupeMap.clear(),this.pendingCounts.clear(),this.errorRateWindow={}}};var Q={event_id:"eid",timestamp:"ts",start_timestamp:"sts",type:"t",level:"l",message:"m",platform:"p",release:"r",environment:"e",fingerprint:"fp",context_id:"cid",user:"u",tags:"tg",exception:"ex",request:"req",contexts:"ctx",breadcrumbs:"bc",transaction:"tx",duration:"d",spans:"sp",extra:"xt"},Ne=Object.fromEntries(Object.entries(Q).map(([s,e])=>[e,s]));function G(s){if(!s||typeof s!="object")return s;let e={};for(let[t,n]of Object.entries(s)){let r=Q[t]||t;e[r]=n}return e}var L=class{constructor(e,t,n){this.useShortFields=!0;this.dsn=e,this.maxRetries=t,this.retryDelay=n}async report(e){if(e.length===0)return;let t=this.buildEnvelope(e),n=0;for(;n<=this.maxRetries;)try{await this.send(t,!1);return}catch(r){let i=r.status;if(i&&i>=400&&i<500||(n++,n>this.maxRetries))throw r;await this.delay(this.retryDelay*Math.pow(2,n-1))}}reportSync(e){if(e.length===0)return;let t=this.buildEnvelope(e);try{if(this.sendSync(t))return}catch(n){}this.sendViaImage(t)}buildEnvelope(e){let t=this.extractSharedMeta(e),n={event_id:this.generateEventId(),sent_at:new Date().toISOString(),meta:t};this.useShortFields&&(n._sf=1);let r=this.applyRelativeTimestamps(e,n),o=[JSON.stringify(n)];for(let a of r){let c=this.stripSharedFields(a,t);this.useShortFields&&(c=G(c));let u=JSON.stringify(c),d=JSON.stringify({type:this.getEnvelopeType(a),length:u.length});o.push(d,u)}return o.join(`
-`)}applyRelativeTimestamps(e,t){if(e.length<=1)return e;let n=[],r=e[0].timestamp?new Date(e[0].timestamp).getTime():Date.now();t._rt=1,t._bt=r;for(let i=0;io.release===t.release)&&(n.release=t.release),t.environment&&e.every(o=>o.environment===t.environment)&&(n.environment=t.environment),t.user&&e.every(o=>JSON.stringify(o.user)===JSON.stringify(t.user))&&(n.user=t.user);let r=t.env;return r&&(n.env=r),n}stripSharedFields(e,t){let n={...e};return t.release&&n.release===t.release&&delete n.release,t.environment&&n.environment===t.environment&&delete n.environment,t.user&&JSON.stringify(n.user)===JSON.stringify(t.user)&&delete n.user,t.env&&delete n.env,n}getEnvelopeType(e){switch(e.type){case"error":return"event";case"performance":return"transaction";default:return e.type}}generateEventId(){return"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".replace(/[x]/g,()=>(Math.random()*16|0).toString(16))}async send(e,t=!1){let n=b(this.dsn);if(navigator.sendBeacon)try{let r=new Blob([e],{type:"application/x-sentry-envelope"});if(navigator.sendBeacon(n,r))return}catch(r){}if(typeof fetch=="function")try{let r=await fetch(n,{method:"POST",body:e,headers:{"Content-Type":"application/x-sentry-envelope"},keepalive:!0});if(r.ok)return;let i=new Error(`HTTP ${r.status}`);throw i.status=r.status,i}catch(r){throw r}if(typeof XMLHttpRequest!="undefined")return new Promise((r,i)=>{let o=new XMLHttpRequest;o.open("POST",n,!t),o.setRequestHeader("Content-Type","application/x-sentry-envelope"),o.onload=()=>{if(o.status>=200&&o.status<300)r();else{let a=new Error(`HTTP ${o.status}`);a.status=o.status,i(a)}},o.onerror=()=>i(new Error("Network error")),o.send(e)});throw new Error("No transport available")}sendSync(e){let t=b(this.dsn);if(navigator.sendBeacon)try{let n=new Blob([e],{type:"application/x-sentry-envelope"});return navigator.sendBeacon(t,n)}catch(n){return!1}if(typeof XMLHttpRequest!="undefined")try{let n=new XMLHttpRequest;return n.open("POST",t,!1),n.setRequestHeader("Content-Type","application/x-sentry-envelope"),n.send(e),n.status>=200&&n.status<300}catch(n){return!1}return!1}sendViaImage(e){try{let t=b(this.dsn),n=new Image,r=encodeURIComponent(btoa(e));return n.src=t+"&sentry_data="+r.substring(0,2e3),!0}catch(t){return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};var k=class{constructor(e){this.plugins=new Map;this.client=e}add(e){if(!this.plugins.has(e.name)){this.plugins.set(e.name,e);try{e.setup(this.client)}catch(t){console.error(`[LightSDK] Plugin "${e.name}" setup error:`,t)}}}remove(e){let t=this.plugins.get(e);if(t){if(t.destroy)try{t.destroy()}catch(n){console.error(`[LightSDK] Plugin "${e}" destroy error:`,n)}this.plugins.delete(e)}}get(e){return this.plugins.get(e)}has(e){return this.plugins.has(e)}applyBeforeReport(e){let t=e;for(let n of this.plugins.values())if(n.beforeReport)try{let r=n.beforeReport(t);if(r===null)return null;t=r}catch(r){console.error(`[LightSDK] Plugin "${n.name}" beforeReport error:`,r)}return t}applyAfterReport(e){for(let t of this.plugins.values())if(t.afterReport)try{t.afterReport(e)}catch(n){console.error(`[LightSDK] Plugin "${t.name}" afterReport error:`,n)}}destroy(){for(let e of this.plugins.values())if(e.destroy)try{e.destroy()}catch(t){console.error(`[LightSDK] Plugin "${e.name}" destroy error:`,t)}this.plugins.clear()}};var _=class{constructor(e){this.breadcrumbs=[];this.maxBreadcrumbs=20;this.enabled=!0;var t,n,r,i;if(!e.dsn)throw new Error("DSN is required");this.dsn=A(e.dsn),this.configManager=new S(e),this.config=this.configManager.getAll(),this.eventBus=new x,this.reporter=new L(this.dsn,(t=e.maxRetries)!=null?t:3,(n=e.retryDelay)!=null?n:1e3),this.pluginManager=new k(this),this.queue=new R((r=e.maxQueueSize)!=null?r:100,(i=e.flushInterval)!=null?i:5e3,this.eventBus,async o=>this.flushEvents(o),o=>this.syncFlushEvents(o)),this.enabled=e.enabled!==!1}init(){this.enabled&&(this.queue.start(),this.emit("ready"))}async flushEvents(e){let t=[];for(let i of e){let o=this.pluginManager.applyBeforeReport(i);o&&t.push(o)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let o=this.configManager.applyToEvent(i,n);n=!1;let a=this.config.get("beforeSend")?this.config.get("beforeSend")(o):o;a&&r.push(a)}if(r.length!==0){await this.reporter.report(r),this.configManager.markContextReported();for(let i of r)this.pluginManager.applyAfterReport(i)}}syncFlushEvents(e){let t=[];for(let i of e){let o=this.pluginManager.applyBeforeReport(i);o&&t.push(o)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let o=this.configManager.applyToEvent(i,n);n=!1;let a=this.config.get("beforeSend")?this.config.get("beforeSend")(o):o;a&&r.push(a)}r.length!==0&&(this.reporter.reportSync(r),this.configManager.markContextReported())}on(e,t){this.eventBus.on(e,t)}off(e,t){this.eventBus.off(e,t)}emit(e,...t){this.eventBus.emit(e,...t)}captureException(e){if(!this.enabled||!this.configManager.shouldSample())return;let t=this.buildErrorEvent(e);this.configManager.isIgnoredError(t.message)||(t.breadcrumbs=[...this.breadcrumbs],this.queue.enqueue(t))}captureMessage(e,t="info"){if(!this.enabled||!this.configManager.shouldSample())return;let n={type:"error",level:t,message:e,timestamp:p(),breadcrumbs:[...this.breadcrumbs]};this.configManager.isIgnoredError(e)||this.queue.enqueue(n)}captureEvent(e){if(!this.enabled||!this.configManager.shouldSample())return;let t={timestamp:p(),level:"info",...e};this.queue.enqueue(t)}buildErrorEvent(e){let t=p();if(e instanceof Error){let r=this.parseStackTrace(e.stack),i=E(e.name,e.message,r);return{type:"error",level:"error",message:e.message,timestamp:t,exception:{type:e.name,value:e.message,stacktrace:{frames:r.slice(0,5)}},fingerprint:i}}return{type:"error",level:"error",message:String(e),timestamp:t}}parseStackTrace(e){if(!e)return[];let t=[],n=e.split(`
-`);for(let r of n){let i=r.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(i){let[,o,a,c,u]=i;t.push({filename:a,function:o,lineno:parseInt(c,10),colno:parseInt(u,10),in_app:!a.includes("node_modules")})}else{let o=r.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(o){let[,a,c,u]=o;t.push({filename:a,lineno:parseInt(c,10),colno:parseInt(u,10),in_app:!a.includes("node_modules")})}}}return t.reverse()}setUser(e){this.configManager.setUser(e)}setTag(e,t){this.configManager.setTag(e,t)}setTags(e){this.configManager.setTags(e)}setExtra(e,t){this.configManager.setExtra(e,t)}addBreadcrumb(e){let t={...e,timestamp:p()};this.breadcrumbs.push(t),this.breadcrumbs.length>this.maxBreadcrumbs&&this.breadcrumbs.shift()}async flush(){await this.queue.flush()}disable(){this.enabled=!1}enable(){this.enabled=!0}use(e){e&&typeof e=="object"&&"name"in e&&"setup"in e&&this.pluginManager.add(e)}destroy(){this.queue.destroy(),this.pluginManager.destroy(),this.eventBus.destroy(),this.breadcrumbs=[]}},B=_;var D=class{constructor(){this.name="error";this.version="1.0.0";this.config={maxStackFrames:5,captureNodeModules:!1,captureColumn:!0,relativePathOnly:!1}}setup(e){this.client=e,this.loadConfig(),this.setupGlobalError(),this.setupUnhandledRejection(),this.setupResourceError()}loadConfig(){let e=this.client.config;e&&v(e.error)&&(this.config={...this.config,...e.error}),e.ignoreErrors&&(this.config.ignoreErrors=[...this.config.ignoreErrors||[],...e.ignoreErrors]),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnoreError(e){return!this.config.ignoreErrors||!e?!1:this.config.ignoreErrors.some(t=>typeof t=="string"?e.includes(t):t.test(e))}shouldIgnoreScriptUrl(e){return w(e,this.config.ignoreUrls)}setupGlobalError(){if(typeof window=="undefined")return;let e=window.onerror;window.onerror=(t,n,r,i,o)=>{try{e&&e.call(window,t,n,r,i,o);let a=this.buildErrorEvent(o||t,n,r,i);a&&this.client.captureEvent(a)}catch(a){console.error("[LightSDK] ErrorPlugin global error handler error:",a)}return!1}}setupUnhandledRejection(){typeof window!="undefined"&&window.addEventListener("unhandledrejection",e=>{try{let t=e.reason,n=this.buildErrorEvent(t);n&&(n.type==="error"&&(n.tags={...n.tags||{},unhandled:"true",promise:"true"}),this.client.captureEvent(n))}catch(t){console.error("[LightSDK] ErrorPlugin unhandledrejection handler error:",t)}})}setupResourceError(){typeof window!="undefined"&&window.addEventListener("error",e=>{var i;let t=e.target;if(!t)return;let n=(i=t.tagName)==null?void 0:i.toLowerCase(),r=t.src||t.href;if(n&&r&&["img","script","link","audio","video"].includes(n))try{this.client.captureEvent({type:"error",level:"warning",message:`Resource load failed: ${n} ${r}`,timestamp:p(),tags:{resource_type:n,resource_url:r},exception:{type:"ResourceError",value:`Failed to load ${n} resource`}})}catch(o){console.error("[LightSDK] ErrorPlugin resource error handler error:",o)}},!0)}buildErrorEvent(e,t,n,r){let i=p();if(e instanceof Error){if(this.shouldIgnoreError(e.message))return null;let c=this.parseStackTrace(e.stack),u=E(e.name,e.message,c);return{type:"error",level:"error",message:e.message,timestamp:i,exception:{type:e.name,value:e.message,stacktrace:{frames:c.slice(0,this.config.maxStackFrames||5)}},fingerprint:u}}let o=String(e);if(this.shouldIgnoreError(o))return null;let a=[];return t&&a.push({filename:t,lineno:n,colno:this.config.captureColumn?r:void 0,in_app:!0}),{type:"error",level:"error",message:o,timestamp:i,exception:{type:"Error",value:o,stacktrace:{frames:a}}}}parseStackTrace(e){if(!e)return[];let t=[],n=e.split(`
-`),r=typeof location!="undefined"?`${location.protocol}//${location.host}`:"";for(let i of n){let o=i.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(o){let[,a,c,u,d]=o,f=c.includes("node_modules");if(!this.config.captureNodeModules&&f)continue;let h=c;this.config.relativePathOnly&&r&&c.startsWith(r)&&(h=c.substring(r.length)),t.push({filename:h,function:a,lineno:parseInt(u,10),colno:this.config.captureColumn?parseInt(d,10):void 0,in_app:!f})}else{let a=i.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(a){let[,c,u,d]=a,f=c.includes("node_modules");if(!this.config.captureNodeModules&&f)continue;let h=c;this.config.relativePathOnly&&r&&c.startsWith(r)&&(h=c.substring(r.length)),t.push({filename:h,lineno:parseInt(u,10),colno:this.config.captureColumn?parseInt(d,10):void 0,in_app:!f})}}}return t.reverse()}destroy(){}},O=D;var N=class{constructor(){this.name="performance";this.version="1.0.0";this.config={sampleRate:.1,captureLongTasks:!0,captureResources:!1,resourceSampleRate:.01,longTaskSampleRate:.05,captureNavigation:!0,captureFP:!0}}setup(e){this.client=e,this.loadConfig(),this.shouldSample("default")&&(this.observeWebVitals(),this.observeLongTasks(),this.observeNavigation(),this.observeResources())}loadConfig(){let e=this.client.config;e&&v(e.performance)&&(this.config={...this.config,...e.performance})}shouldSample(e="default"){var n,r,i;let t=(n=this.config.sampleRate)!=null?n:.1;return e==="resource"?t=(r=this.config.resourceSampleRate)!=null?r:.01:e==="longtask"&&(t=(i=this.config.longTaskSampleRate)!=null?i:.05),t>=1?!0:t<=0?!1:Math.random(){let n=t.getEntriesByName("first-paint");n.length>0&&this.reportMetric("FP",n[0].startTime,"ms")}).observe({type:"paint",buffered:!0})}catch(e){}}observeLCP(){try{new PerformanceObserver(t=>{let n=t.getEntries(),r=n[n.length-1];if(r){let i=r.renderTime||r.loadTime||r.startTime;this.reportMetric("LCP",i,"ms")}}).observe({type:"largest-contentful-paint",buffered:!0})}catch(e){}}observeFID(){try{new PerformanceObserver(t=>{let n=t.getEntries();if(n.length>0){let r=n[0],i=r.processingStart-r.startTime;this.reportMetric("FID",i,"ms")}}).observe({type:"first-input",buffered:!0})}catch(e){}}observeCLS(){try{let e=0,t=0,n=[];new PerformanceObserver(i=>{let o=i.getEntries();for(let a of o)if(!a.hadRecentInput){let c=n[0],u=n[n.length-1];t&&a.startTime-u.endTime<1e3&&a.startTime-c.startTime<5e3?(t+=a.value||0,n.push(a)):(t=a.value||0,n=[a]),t>e&&(e=t,this.reportMetric("CLS",e,""))}}).observe({type:"layout-shift",buffered:!0})}catch(e){}}observeFCP(){try{new PerformanceObserver(t=>{let n=t.getEntriesByName("first-contentful-paint");n.length>0&&this.reportMetric("FCP",n[0].startTime,"ms")}).observe({type:"paint",buffered:!0})}catch(e){}}observeTTFB(){try{new PerformanceObserver(t=>{let n=t.getEntriesByType("navigation");if(n.length>0){let r=n[0],i=r.responseStart-r.requestStart;this.reportMetric("TTFB",Math.max(0,i),"ms")}}).observe({type:"navigation",buffered:!0})}catch(e){}}observeLongTasks(){if(this.config.captureLongTasks)try{new PerformanceObserver(t=>{let n=t.getEntries();for(let r of n)this.shouldSample("longtask")&&this.reportMetric("longtask",r.duration,"ms")}).observe({type:"longtask",buffered:!0})}catch(e){}}observeNavigation(){this.config.captureNavigation&&typeof performance!="undefined"&&window.addEventListener("load",()=>{setTimeout(()=>{let e=performance.timing;if(!e)return;let t=e.navigationStart,n={dom_ready:e.domContentLoadedEventEnd-t,load_time:e.loadEventEnd-t,dns:e.domainLookupEnd-e.domainLookupStart,tcp:e.connectEnd-e.connectStart,ssl:e.secureConnectionStart>0?e.connectEnd-e.secureConnectionStart:0,ttfb:e.responseStart-e.requestStart,download:e.responseEnd-e.responseStart,dom_parse:e.domInteractive-e.responseEnd};for(let[r,i]of Object.entries(n))i>0&&this.reportMetric(r,i,"ms")},0)})}observeResources(){if(this.config.captureResources&&typeof performance!="undefined")try{new PerformanceObserver(t=>{if(!this.shouldSample("resource"))return;let n=t.getEntriesByType("resource");for(let r of n)r.duration>1e3&&this.reportMetric("resource_slow",r.duration,"ms",{resource_name:r.name.substring(0,200),resource_type:r.initiatorType})}).observe({type:"resource",buffered:!0})}catch(e){}}getRating(e,t){let r={LCP:{good:2500,poor:4e3},FCP:{good:1800,poor:3e3},FP:{good:1800,poor:3e3},FID:{good:100,poor:300},CLS:{good:.1,poor:.25},TTFB:{good:800,poor:1800}}[e];return!r||t<=r.good?"good":t<=r.poor?"needs-improvement":"poor"}reportMetric(e,t,n,r={}){let i=this.getRating(e,t),o={type:"performance",level:"info",metric:e,value:Math.round(t*100)/100,unit:n,rating:i,timestamp:p(),tags:{metric:e,rating:i,...r}};this.client.captureEvent(o)}destroy(){}},F=N;var q=class{constructor(){this.name="network";this.version="1.0.0";this.config={captureSuccess:!1,captureBody:!1,captureRequestBody:!1,captureResponseBody:!1,captureRequestHeaders:["content-type","user-agent"],captureResponseHeaders:["content-type","content-length"],errorSampleRate:1,successSampleRate:.01,slowThreshold:3e3,slowSampleRate:1,ignoreStatusCodes:[]}}setup(e){this.client=e,this.loadConfig(),this.patchFetch(),this.patchXHR()}loadConfig(){let e=this.client.config;e&&v(e.network)&&(this.config={...this.config,...e.network}),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnore(e){return w(e,this.config.ignoreUrls||[])}shouldSample(e,t){var r,i,o,a;let n=e?(r=this.config.errorSampleRate)!=null?r:1:(i=this.config.successSampleRate)!=null?i:.01;return!e&&t>=((o=this.config.slowThreshold)!=null?o:3e3)&&(n=(a=this.config.slowSampleRate)!=null?a:1),n>=1?!0:n<=0?!1:Math.random(){var y;let d=p()-r,f=u.ok,h=u.status;return(y=e.config.ignoreStatusCodes)!=null&&y.includes(h)||!e.shouldSample(!f,d)||(!f||e.config.captureSuccess)&&e.reportNetwork({sub_type:"fetch",method:o.toUpperCase(),url:a,status_code:h,duration:d,success:f,error:f?void 0:`HTTP ${h}`}),u},u=>{let d=p()-r;throw e.shouldSample(!0,d)&&e.reportNetwork({sub_type:"fetch",method:o.toUpperCase(),url:a,duration:d,success:!1,error:(u==null?void 0:u.message)||"Network error"}),u}),c}}patchXHR(){if(typeof XMLHttpRequest=="undefined")return;this.originalXHROpen=XMLHttpRequest.prototype.open,this.originalXHRSend=XMLHttpRequest.prototype.send;let e=this;XMLHttpRequest.prototype.open=function(t,n){return this._lightMethod=t,this._lightUrl=typeof n=="string"?n:n.toString(),e.originalXHROpen.apply(this,arguments)},XMLHttpRequest.prototype.send=function(t){let n=p(),r=this._lightMethod||"GET",i=this._lightUrl||"";if(e.shouldIgnore(i))return e.originalXHRSend.apply(this,arguments);let o=M(i),a=0;t&&typeof t=="string"&&(a=t.length);let c=()=>{var y;let u=p()-n,d=this.status,f=d>=200&&d<300;if((y=e.config.ignoreStatusCodes)!=null&&y.includes(d)){this.removeEventListener("loadend",c);return}if(!e.shouldSample(!f,u)){this.removeEventListener("loadend",c);return}if(f&&!e.config.captureSuccess){this.removeEventListener("loadend",c);return}let h=0;try{let P=this.getResponseHeader("content-length");P&&(h=parseInt(P,10)),!h&&this.responseText&&(h=this.responseText.length)}catch(P){}e.reportNetwork({sub_type:"xhr",method:r.toUpperCase(),url:o,status_code:d,duration:u,request_size:a||void 0,response_size:h||void 0,success:f,error:f?void 0:`HTTP ${d}`}),this.removeEventListener("loadend",c)};return this.addEventListener("loadend",c),e.originalXHRSend.apply(this,arguments)}}reportNetwork(e){let t={type:"network",level:e.success?"info":"error",timestamp:p(),tags:{method:e.method,sub_type:e.sub_type,success:String(e.success)},...e};this.client.captureEvent(t)}destroy(){this.originalFetch&&(window.fetch=this.originalFetch),this.originalXHROpen&&(XMLHttpRequest.prototype.open=this.originalXHROpen),this.originalXHRSend&&(XMLHttpRequest.prototype.send=this.originalXHRSend)}},U=q;var H=class{constructor(){this.name="behavior";this.version="1.0.0";this.config={capturePV:!0,captureClick:!0,captureRoute:!0,captureDuration:!0,captureScroll:!0,clickThrottle:300,scrollThrottle:1e3,sampleRate:.1};this.lastClickTime=0;this.lastScrollTime=0;this.maxScrollDepth=0;this.pageEnterTime=0;this.scrollReported=!1}setup(e){this.client=e,this.loadConfig(),this.shouldSample()&&(this.config.capturePV&&this.trackPV(),this.config.captureClick&&this.trackClick(),this.config.captureRoute&&this.trackRoute(),this.config.captureDuration&&this.trackPageDuration(),this.config.captureScroll&&this.trackScroll())}loadConfig(){let e=this.client.config;e&&v(e.behavior)&&(this.config={...this.config,...e.behavior})}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:.1;return e>=1?!0:e<=0?!1:Math.random(){var a,c,u;let t=p(),n=(a=this.config.clickThrottle)!=null?a:300;if(t-this.lastClickTime{let i=g();if(i!==e){let o=e,a=i;e=i,this.reportBehavior({sub_type:"route",page_url:a,referrer:o,properties:{from:o,to:a}}),this.pageEnterTime=p(),this.maxScrollDepth=0,this.scrollReported=!1}},n=history.pushState,r=history.replaceState;history.pushState=function(){let i=n.apply(this,arguments);return setTimeout(t,0),i},history.replaceState=function(){let i=r.apply(this,arguments);return setTimeout(t,0),i},window.addEventListener("popstate",()=>{setTimeout(t,0)}),window.addEventListener("hashchange",()=>{setTimeout(t,0)})}trackPageDuration(){if(typeof window=="undefined")return;this.pageEnterTime=p();let e=()=>{let t=p()-this.pageEnterTime;t>1e3&&(this.config.captureScroll&&!this.scrollReported&&this.reportScroll(),this.reportBehavior({sub_type:"duration",page_url:g(),properties:{duration:t,max_scroll_depth:this.maxScrollDepth}}))};document.addEventListener("visibilitychange",()=>{document.hidden?e():this.pageEnterTime=p()}),window.addEventListener("beforeunload",e),window.addEventListener("pagehide",e)}trackScroll(){if(typeof window=="undefined")return;let e=()=>{var i;let t=p(),n=(i=this.config.scrollThrottle)!=null?i:1e3;if(t-this.lastScrollTimethis.maxScrollDepth&&(this.maxScrollDepth=r)};window.addEventListener("scroll",e,{passive:!0})}reportScroll(){this.scrollReported||(this.scrollReported=!0,this.reportBehavior({sub_type:"scroll",page_url:g(),properties:{max_depth:this.maxScrollDepth}}))}reportBehavior(e){let t={type:"behavior",level:"info",timestamp:p(),tags:{sub_type:e.sub_type,page_url:g()},...e};this.client.captureEvent(t)}destroy(){}},$=H;var Se="light-sentry-offline",m="events",W=1e3,j=class{constructor(){this.name="offline";this.version="1.0.0";this.config={maxEvents:W};this.db=null;this.isOnline=!0;this.isSyncing=!1;this.pendingEvents=[]}setup(e){this.client=e,this.loadConfig(),this.initDatabase(),this.setupNetworkListeners()}loadConfig(){let e=this.client.config;e&&typeof e.offline=="object"&&(this.config={...this.config,...e.offline})}async initDatabase(){if(typeof indexedDB=="undefined"){console.warn("[LightSDK] OfflinePlugin: IndexedDB not available");return}return new Promise((e,t)=>{let n=indexedDB.open(Se,1);n.onerror=()=>{console.warn("[LightSDK] OfflinePlugin: Failed to open IndexedDB"),t(n.error)},n.onsuccess=()=>{this.db=n.result,e()},n.onupgradeneeded=r=>{let i=r.target.result;i.objectStoreNames.contains(m)||i.createObjectStore(m,{keyPath:"id"}).createIndex("timestamp","timestamp",{unique:!1})}})}setupNetworkListeners(){typeof window!="undefined"&&(this.isOnline=navigator.onLine,window.addEventListener("online",()=>{console.info("[LightSDK] OfflinePlugin: Network online"),this.isOnline=!0,this.syncPendingEvents()}),window.addEventListener("offline",()=>{console.info("[LightSDK] OfflinePlugin: Network offline"),this.isOnline=!1}))}generateId(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}async saveToIndexedDB(e){if(this.db)return new Promise((t,n)=>{let i=this.db.transaction([m],"readwrite").objectStore(m),o=i.count();o.onsuccess=async()=>{let a=o.result;a>=(this.config.maxEvents||W)&&await this.deleteOldestEvents(a-(this.config.maxEvents||W)+1);let c={id:this.generateId(),event:{...e,offline:!0},timestamp:p()},u=i.add(c);u.onsuccess=()=>t(),u.onerror=()=>n(u.error)}})}async deleteOldestEvents(e){if(!(!this.db||e<=0))return new Promise((t,n)=>{let a=this.db.transaction([m],"readwrite").objectStore(m).index("timestamp").openCursor(),c=0;a.onsuccess=u=>{let d=u.target.result;d&&cn(a.error)})}async getAllStoredEvents(){return this.db?new Promise((e,t)=>{let o=this.db.transaction([m],"readonly").objectStore(m).index("timestamp").getAll();o.onsuccess=()=>{let a=o.result||[];a.sort((c,u)=>c.timestamp-u.timestamp),e(a)},o.onerror=()=>t(o.error)}):[]}async clearStoredEvents(){if(this.db)return new Promise((e,t)=>{let i=this.db.transaction([m],"readwrite").objectStore(m).clear();i.onsuccess=()=>e(),i.onerror=()=>t(i.error)})}async syncPendingEvents(){if(!(!this.isOnline||this.isSyncing)){this.isSyncing=!0,console.info("[LightSDK] OfflinePlugin: Syncing pending events");try{let e=await this.getAllStoredEvents();if(e.length===0){console.info("[LightSDK] OfflinePlugin: No pending events to sync"),this.isSyncing=!1;return}let t=50;for(let n=0;no.event);try{await(async()=>new Promise((a,c)=>{this.client.emit("sync:offline",i),a()}))(),console.info(`[LightSDK] OfflinePlugin: Synced ${r.length} events`)}catch(o){console.error("[LightSDK] OfflinePlugin: Failed to sync batch",o)}}await this.clearStoredEvents(),console.info("[LightSDK] OfflinePlugin: All pending events synced")}catch(e){console.error("[LightSDK] OfflinePlugin: Sync failed",e)}finally{this.isSyncing=!1}}}beforeReport(e){if(!this.isOnline)return this.saveToIndexedDB(e).catch(n=>{console.error("[LightSDK] OfflinePlugin: Failed to save event offline",n)}),null;let t={...e};return delete t.offline,t}destroy(){this.db&&(this.db.close(),this.db=null)}},K=j;var l=null;function J(s){var r;let e=new B(s),t=[new O,new K],n=((r=s.plugins)==null?void 0:r.filter(i=>typeof i=="string"))||[];(n.includes("performance")||n.includes("all"))&&t.push(new F),(n.includes("network")||n.includes("all"))&&t.push(new U),(n.includes("behavior")||n.includes("all"))&&t.push(new $);for(let i of t)e.use(i);if(s.plugins)for(let i of s.plugins)typeof i=="object"&&i!==null&&"name"in i&&"setup"in i&&e.use(i);return e.init(),l=e,e}function Z(){return l}function Y(s){l==null||l.captureException(s)}function ee(s,e){l==null||l.captureMessage(s,e)}function te(s){l==null||l.captureEvent(s)}function ne(s){l==null||l.setUser(s)}function re(s,e){l==null||l.setTag(s,e)}function ie(s){l==null||l.setTags(s)}function se(s,e){l==null||l.setExtra(s,e)}function oe(s){l==null||l.addBreadcrumb(s)}async function ae(){await(l==null?void 0:l.flush())}function ce(){l==null||l.disable()}function ue(){l==null||l.enable()}var xe={init:J,getClient:Z,captureException:Y,captureMessage:ee,captureEvent:te,setUser:ne,setTag:re,setTags:ie,setExtra:se,addBreadcrumb:oe,flush:ae,disable:ce,enable:ue};return ge(Re);})();
+"use strict";var LightSDK=(()=>{var D=Object.defineProperty;var we=Object.getOwnPropertyDescriptor;var Ee=Object.getOwnPropertyNames;var Se=Object.prototype.hasOwnProperty;var xe=(o,e)=>{for(var t in e)D(o,t,{get:e[t],enumerable:!0})},Re=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of Ee(e))!Se.call(o,r)&&r!==t&&D(o,r,{get:()=>e[r],enumerable:!(n=we(e,r))||n.enumerable});return o};var ke=o=>Re(D({},"__esModule",{value:!0}),o);var De={};xe(De,{BehaviorPlugin:()=>V,Client:()=>q,ErrorPlugin:()=>j,NetworkPlugin:()=>A,OfflinePlugin:()=>G,PerformancePlugin:()=>W,SamplingPlugin:()=>Z,addBreadcrumb:()=>me,captureEvent:()=>de,captureException:()=>ue,captureMessage:()=>le,default:()=>Oe,disable:()=>ye,enable:()=>be,flush:()=>ve,getClient:()=>ce,init:()=>ae,setExtra:()=>ge,setTag:()=>he,setTags:()=>fe,setUser:()=>pe});function ee(o){try{let e=o.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/);if(!e)throw new Error("Invalid DSN format");let[,t,n,r,i]=e;return{protocol:t,publicKey:n,host:r,projectId:i}}catch(e){throw new Error(`Failed to parse DSN: ${e.message}`)}}function k(o){return`${o.protocol}://${o.host}/api/${o.projectId}/envelope/?sentry_key=${o.publicKey}`}function te(){return"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".replace(/[x]/g,()=>(Math.random()*16|0).toString(16))}function d(){return Date.now()}function y(o){return o!==null&&typeof o=="object"&&!Array.isArray(o)}function T(o,e){return!e||e.length===0?!1:e.some(t=>typeof t=="string"?o.includes(t):t.test(o))}function Te(o){let e=5381,t=o.length;for(;t;)e=e*33^o.charCodeAt(--t);return e>>>0}function F(o){return Te(o).toString(16)}function L(o,e,t=[]){let n=t.slice(0,3).map(i=>`${i.filename}:${i.lineno||0}`).join("|"),r=e.replace(/['"]?\d+['"]?/g,"").replace(/\s+/g," ").trim();return F(`${o}:${r}:${n}`)}function P(o,e={}){if(!o)return[];let{captureNodeModules:t=!1,captureColumn:n=!0,relativePathOnly:r=!1,maxFrames:i}=e,s=[],a=o.split(`
+`),c=typeof location!="undefined"?`${location.protocol}//${location.host}`:"";for(let p of a){let w=p.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);if(w){let[,f,h,v,g]=w,E=h.includes("node_modules");if(!t&&E)continue;let S=h;r&&c&&h.startsWith(c)&&(S=h.substring(c.length)),s.push({filename:S,function:f,lineno:parseInt(v,10),colno:n?parseInt(g,10):void 0,in_app:!E})}else{let f=p.match(/^\s*at\s*(.+?)@(.+?):(\d+):(\d+)\s*$/);if(f){let[,v,g,E,S]=f,R=g.includes("node_modules");if(!t&&R)continue;let Y=g;r&&c&&g.startsWith(c)&&(Y=g.substring(c.length)),s.push({filename:Y,function:v||"",lineno:parseInt(E,10),colno:n?parseInt(S,10):void 0,in_app:!R});continue}let h=p.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);if(h){let[,v,g,E]=h,S=v.includes("node_modules");if(!t&&S)continue;let R=v;r&&c&&v.startsWith(c)&&(R=v.substring(c.length)),s.push({filename:R,lineno:parseInt(g,10),colno:n?parseInt(E,10):void 0,in_app:!S})}}}let u=s.reverse();return i!==void 0&&i>0?u.slice(0,i):u}function Le(o){let e=[{name:"WeChat",pattern:/MicroMessenger\/([\d.]+)/i},{name:"QQ Browser",pattern:/QIBrowser\/([\d.]+)/i},{name:"UC Browser",pattern:/UCBrowser\/([\d.]+)/i},{name:"Edge",pattern:/Edg\/([\d.]+)/i},{name:"Edge",pattern:/Edge\/([\d.]+)/i},{name:"Opera",pattern:/OPR\/([\d.]+)/i},{name:"Opera",pattern:/Opera\/([\d.]+)/i},{name:"Firefox",pattern:/Firefox\/([\d.]+)/i},{name:"Safari",pattern:/Version\/([\d.]+).*Safari/i},{name:"Chrome",pattern:/Chrome\/([\d.]+)/i},{name:"IE",pattern:/MSIE ([\d.]+)/i},{name:"IE",pattern:/Trident.*rv:([\d.]+)/i}];for(let t of e){let n=o.match(t.pattern);if(n){let r=n[1]||"",i=r.split(".")[0]||"";return{name:t.name,version:r,major:i}}}return{name:"Unknown",version:"",major:""}}function Pe(o){let e=[{name:"Windows 11",pattern:/Windows NT 10\.0.*Win64.*x64/i},{name:"Windows 10",pattern:/Windows NT 10\.0/i},{name:"Windows 8.1",pattern:/Windows NT 6\.3/i},{name:"Windows 8",pattern:/Windows NT 6\.2/i},{name:"Windows 7",pattern:/Windows NT 6\.1/i},{name:"Windows Vista",pattern:/Windows NT 6\.0/i},{name:"Windows XP",pattern:/Windows NT 5\.1/i},{name:"macOS",pattern:/Mac OS X ([\d_.]+)/i},{name:"iOS",pattern:/OS ([\d_.]+) like Mac OS X/i},{name:"Android",pattern:/Android ([\d.]+)/i},{name:"Android",pattern:/Android/i},{name:"Linux",pattern:/Linux/i}];for(let t of e){let n=o.match(t.pattern);if(n){let r=n[1]||"";return r&&(r=r.replace(/_/g,".")),{name:t.name,version:r}}}return{name:"Unknown",version:""}}function Ce(o){let e=/Mobile|Android|iPhone|iPod|BlackBerry|Windows Phone|Opera Mini/i.test(o),t=/Tablet|iPad|PlayBook|Kindle|Silk/i.test(o),n="desktop";t?n="tablet":e&&(n="mobile");let r="",i="",s=o.match(/iPhone/i),a=o.match(/iPad/i),c=o.match(/SM-([A-Z0-9]+)/i),u=o.match(/Pixel ([0-9A-Z]+)/i),p=o.match(/HUAWEI ([A-Z0-9]+)/i);return s?(r="Apple",i="iPhone"):a?(r="Apple",i="iPad"):c?(r="Samsung",i=c[1]):u?(r="Google",i=`Pixel ${u[1]}`):p&&(r="Huawei",i=p[1]),{type:n,vendor:r,model:i}}function ne(){let o=typeof navigator!="undefined"?navigator.userAgent:"",e=Le(o),t=Pe(o),n=Ce(o);return{browser:e,os:t,device:n,url:typeof location!="undefined"?location.href:"",referrer:typeof document!="undefined"?document.referrer:"",title:typeof document!="undefined"?document.title:"",language:typeof navigator!="undefined"?navigator.language:"",user_agent:o,screen_width:typeof screen!="undefined"?screen.width:0,screen_height:typeof screen!="undefined"?screen.height:0,viewport_width:typeof window!="undefined"?window.innerWidth:0,viewport_height:typeof window!="undefined"?window.innerHeight:0}}function b(){return typeof location!="undefined"?location.href:""}function re(){return typeof document!="undefined"?document.referrer:""}function x(o){if(!o)return o;try{let e=new URL(o),t=["password","passwd","pwd","token","access_token","refresh_token","api_key","apikey","key","secret","private_key","code","auth","id_token","idToken","session","session_id"],n=e.searchParams,r=!1;for(let i of Array.from(n.keys())){let s=i.toLowerCase().replace(/[-_]/g,"_");t.some(a=>s.includes(a))&&(n.set(i,"[Filtered]"),r=!0)}if(e.hash&&e.hash.length>1){let i=e.hash.substring(1);if(i.includes("=")){let s=new URLSearchParams(i),a=!1;for(let c of Array.from(s.keys())){let u=c.toLowerCase().replace(/[-_]/g,"_");t.some(p=>u.includes(p))&&(s.set(c,"[Filtered]"),a=!0)}a&&(e.hash="#"+s.toString(),r=!0)}}return r?e.toString():o}catch(e){return o}}function C(o,e=[]){return!e||e.length===0?!1:e.some(t=>{if(typeof t=="string"){if(t.includes("*")){let n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(n).test(o)}return o.includes(t)}return t.test(o)})}var Me={Chrome:"c",Safari:"s",Firefox:"f",Edge:"e",Opera:"o",IE:"i",WeChat:"w","QQ Browser":"q","UC Browser":"u","Mobile Chrome":"cm","Mobile Safari":"sm"},Be={"Windows 11":"w11","Windows 10":"w10","Windows 8.1":"w81","Windows 8":"w8","Windows 7":"w7","Windows Vista":"wv","Windows XP":"wxp",macOS:"m",iOS:"i",Android:"a",Linux:"l"};function U(){let o=ne(),e=Me[o.browser.name]||o.browser.name.toLowerCase(),t=Be[o.os.name]||o.os.name.toLowerCase();return{browser:{name:e,version:o.browser.major},os:{name:t,version:o.os.version.split(".")[0]},device:{type:o.device.type,brand:o.device.vendor||void 0,model:o.device.model||void 0}}}function ie(){let o=ne(),e=`${o.browser.name}|${o.browser.major}|${o.os.name}|${o.os.version.split(".")[0]}|${o.device.type}|${o.device.vendor}|${o.device.model}|${o.language}`;return F(e).substring(0,16)}var Ie={enabled:!0,sampleRate:1,maxQueueSize:100,flushInterval:5e3,maxRetries:3,retryDelay:1e3,environment:"production",contextLevel:{fatal:{maxStackFrames:5,maxBreadcrumbs:20},error:{maxStackFrames:5,maxBreadcrumbs:10},warning:{maxStackFrames:3,maxBreadcrumbs:5},info:{maxStackFrames:0,maxBreadcrumbs:0},debug:{maxStackFrames:0,maxBreadcrumbs:0}}},M=class{constructor(e){this.contexts=null;this.contextId=null;this.contextReported=!1;this.config={...Ie,...e}}markContextReported(){this.contextReported=!0}get(e){return this.config[e]}getAll(){return{...this.config}}set(e,t){this.config[e]=t}setUser(e){this.config.user=e||void 0}setTags(e){this.config.tags={...this.config.tags||{},...e}}setTag(e,t){this.config.tags||(this.config.tags={}),this.config.tags[e]=t}setExtra(e,t){this.config.extra||(this.config.extra={}),this.config.extra[e]=t}isIgnoredError(e){return T(e,this.config.ignoreErrors||[])}getEncodedContext(){return this.contexts||(this.contexts=U()),this.contexts}applyToEvent(e,t=!0){var i,s,a,c;let n={...e};if(e.tags&&(n.tags={...e.tags}),e.request&&(n.request={...e.request}),"exception"in e&&e.exception){let u=e.exception;n.exception={...u,stacktrace:u.stacktrace?{...u.stacktrace,frames:u.stacktrace.frames?u.stacktrace.frames.map(p=>({...p})):void 0}:void 0}}e.breadcrumbs&&(n.breadcrumbs=e.breadcrumbs.map(u=>({...u}))),e.user&&(n.user={...e.user}),e.contexts&&(n.contexts={...e.contexts}),this.config.release&&(n.release=this.config.release),this.config.environment&&(n.environment=this.config.environment),this.config.user&&(n.user=this.config.user),this.config.tags&&(n.tags={...this.config.tags,...e.tags||{}}),n.request?n.request.url||(n.request.url=b()):n.request={url:b(),referrer:re()},this.contextId||(this.contextId=ie()),n.context_id=this.contextId;let r=this.applyContextLevel(n);return t&&!this.contextReported&&(this.contexts||(this.contexts=U()),r.contexts=this.contexts,r.env={b:(i=this.contexts.browser)==null?void 0:i.name,bv:(s=this.contexts.browser)==null?void 0:s.version,os:(a=this.contexts.os)==null?void 0:a.name,osv:(c=this.contexts.os)==null?void 0:c.version}),r}applyContextLevel(e){let t=this.config.contextLevel;if(!t)return e;let n=e.level||"error",r=t[n]||t.error,i={...e};if("exception"in e&&e.exception&&typeof e.exception=="object"){let s=e.exception;if(s.stacktrace&&typeof s.stacktrace=="object"){let a=s.stacktrace;Array.isArray(a.frames)&&(r.maxStackFrames===0?delete i.exception:i.exception={...s,stacktrace:{...a,frames:a.frames.slice(0,r.maxStackFrames)}})}}return e.breadcrumbs&&Array.isArray(e.breadcrumbs)&&(r.maxBreadcrumbs===0?delete i.breadcrumbs:i.breadcrumbs=e.breadcrumbs.slice(-r.maxBreadcrumbs)),i}};var B=class{constructor(){this.handlers=new Map}on(e,t){this.handlers.has(e)||this.handlers.set(e,[]),this.handlers.get(e).push(t)}off(e,t){let n=this.handlers.get(e);if(n){let r=n.indexOf(t);r>-1&&n.splice(r,1)}}emit(e,...t){let n=this.handlers.get(e);if(n)for(let r of n)try{r(...t)}catch(i){console.error("[LightSDK] EventBus handler error:",i)}}once(e,t){let n=(...r)=>{this.off(e,n),t(...r)};this.on(e,n)}destroy(){this.handlers.clear()}};var I=class{constructor(e,t,n,r,i){this.queue=[];this.timer=null;this.lastFlushTime=0;this.isFlushing=!1;this.dedupeMap=new Map;this.pendingCounts=new Map;this.errorRateWindow={};this.paused=!1;this.pauseTimer=null;this.maxSize=e,this.flushInterval=t,this.eventBus=n,this.flushCallback=r,this.syncFlushCallback=i}start(){this.startTimer(),this.setupVisibilityListener()}startTimer(){this.timer||(this.timer=setInterval(()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flush()},this.flushInterval))}setupVisibilityListener(){this.onVisibilityChange=()=>{document.hidden&&(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},typeof document!="undefined"&&document.addEventListener("visibilitychange",this.onVisibilityChange),this.onBeforeUnload=()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},this.onPagehide=()=>{(this.queue.length>0||this.pendingCounts.size>0)&&this.flushSync()},typeof window!="undefined"&&(window.addEventListener("beforeunload",this.onBeforeUnload),window.addEventListener("pagehide",this.onPagehide))}checkInfiniteLoop(e){let t=d(),n=t-1e3;this.errorRateWindow[e]||(this.errorRateWindow[e]=[]);let r=this.errorRateWindow[e];for(r.push(t);r.length>0&&r[0]10?(this.paused||(console.warn("[LightSDK] Infinite loop detected, pausing SDK for 60s",{fingerprint:e,count:r.length}),this.eventBus.emit("error",new Error("Infinite loop detected")),this.paused=!0,this.pauseTimer&&clearTimeout(this.pauseTimer),this.pauseTimer=setTimeout(()=>{this.paused=!1,this.errorRateWindow={},console.info("[LightSDK] Resumed after infinite loop detection")},6e4)),!0):!1}enqueue(e){if(this.paused)return;this.cleanupExpiredDedupeEntries();let t=this.getDedupeKey(e);if(t){if(this.checkInfiniteLoop(t))return;let n=d(),r=this.dedupeMap.get(t);if(r)if(n-r.lastTime<6e4){if(r.count>=3){if(!this.pendingCounts.has(t))this.pendingCounts.set(t,{count:1,ts_start:n,ts_end:n});else{let i=this.pendingCounts.get(t);i.count++,i.ts_end=n}return}r.count++,r.lastTime=n}else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!0}),this.pendingCounts.delete(t);else this.dedupeMap.set(t,{count:1,lastTime:n,firstReported:!1})}this.queue.push(e),this.eventBus.emit("event",e),this.queue.length>=this.maxSize&&this.flush()}cleanupExpiredDedupeEntries(){if(this.dedupeMap.size<100&&Math.random()>.1)return;let e=d(),t=6e4;for(let[n,r]of this.dedupeMap)e-r.lastTime>t&&this.dedupeMap.delete(n)}buildCountEvents(){let e=[];for(let[t,n]of this.pendingCounts.entries()){let r={type:"count",fingerprint:t,count:n.count,ts_start:n.ts_start,ts_end:n.ts_end,timestamp:d(),level:"info"};e.push(r)}return e}getDedupeKey(e){if(e.type==="error"){let t=e;return t.fingerprint||t.message}return null}async flush(){if(this.queue.length===0&&this.pendingCounts.size===0||this.isFlushing)return;this.isFlushing=!0;let e=this.queue.splice(0,this.queue.length),t=this.buildCountEvents(),n=[...e,...t];this.lastFlushTime=d(),this.eventBus.emit("report",n);try{await this.flushCallback(n),this.pendingCounts.clear(),this.eventBus.emit("reported",n)}catch(r){throw e.forEach(i=>this.queue.unshift(i)),r}finally{this.isFlushing=!1}}flushSync(){if(this.queue.length===0&&this.pendingCounts.size===0)return;let e=this.queue.splice(0,this.queue.length),t=new Map(this.pendingCounts),n=this.buildCountEvents(),r=[...e,...n];this.lastFlushTime=d(),this.eventBus.emit("report",r);try{this.syncFlushCallback(r),this.pendingCounts.clear(),this.eventBus.emit("reported",r)}catch(i){console.error("[LightSDK] Sync flush failed",i),e.forEach(s=>this.queue.unshift(s));for(let[s,a]of t)this.pendingCounts.has(s)||this.pendingCounts.set(s,a)}}size(){return this.queue.length}destroy(){this.timer&&(clearInterval(this.timer),this.timer=null),this.pauseTimer&&(clearTimeout(this.pauseTimer),this.pauseTimer=null),typeof document!="undefined"&&this.onVisibilityChange&&document.removeEventListener("visibilitychange",this.onVisibilityChange),typeof window!="undefined"&&(this.onBeforeUnload&&window.removeEventListener("beforeunload",this.onBeforeUnload),this.onPagehide&&window.removeEventListener("pagehide",this.onPagehide)),this.dedupeMap.clear(),this.pendingCounts.clear(),this.errorRateWindow={}}};var se={event_id:"eid",timestamp:"ts",start_timestamp:"sts",type:"t",level:"l",message:"m",platform:"p",release:"r",environment:"e",fingerprint:"fp",context_id:"cid",user:"u",tags:"tg",exception:"ex",request:"req",contexts:"ctx",breadcrumbs:"bc",transaction:"tx",duration:"d",spans:"sp",extra:"xt"},Qe=Object.fromEntries(Object.entries(se).map(([o,e])=>[e,o]));function oe(o){if(!o||typeof o!="object")return o;let e={};for(let[t,n]of Object.entries(o)){let r=se[t]||t;e[r]=n}return e}var _=class{constructor(e,t,n){this.useShortFields=!0;this.dsn=e,this.maxRetries=t,this.retryDelay=n}async report(e){if(e.length===0)return;let t=this.buildEnvelope(e),n=0;for(;n<=this.maxRetries;)try{await this.send(t,!1);return}catch(r){let i=r.status;if(i&&i>=400&&i<500||(n++,n>this.maxRetries))throw r;let s=Math.min(this.retryDelay*Math.pow(2,n-1),5e3);await this.delay(s)}}reportSync(e){if(e.length===0)return;let t=this.buildEnvelope(e);try{if(this.sendSync(t))return}catch(n){}this.sendViaImage(t)}buildEnvelope(e){let t=this.extractSharedMeta(e),n={event_id:te(),sent_at:new Date().toISOString(),meta:t};this.useShortFields&&(n._sf=1);let r=this.applyRelativeTimestamps(e,n),s=[JSON.stringify(n)];for(let a of r){let c=this.stripSharedFields(a,t);this.useShortFields&&(c=oe(c));let u=JSON.stringify(c),p=JSON.stringify({type:this.getEnvelopeType(a),length:u.length});s.push(p,u)}return s.join(`
+`)}applyRelativeTimestamps(e,t){if(e.length<=1)return e;let n=[],r=e[0].timestamp?new Date(e[0].timestamp).getTime():Date.now();t._rt=1,t._bt=r;for(let i=0;is.release===t.release)&&(n.release=t.release),t.environment&&e.every(s=>s.environment===t.environment)&&(n.environment=t.environment),t.user&&e.every(s=>JSON.stringify(s.user)===JSON.stringify(t.user))&&(n.user=t.user);let r=t.env;return r&&(n.env=r),n}stripSharedFields(e,t){let n={...e};return t.release&&n.release===t.release&&delete n.release,t.environment&&n.environment===t.environment&&delete n.environment,t.user&&JSON.stringify(n.user)===JSON.stringify(t.user)&&delete n.user,t.env&&delete n.env,n}getEnvelopeType(e){switch(e.type){case"error":return"event";case"performance":return"transaction";default:return e.type}}async send(e,t=!1){let n=k(this.dsn);if(navigator.sendBeacon)try{let r=new Blob([e],{type:"application/x-sentry-envelope"});if(navigator.sendBeacon(n,r))return}catch(r){}if(typeof fetch=="function")try{let r=new AbortController,i=setTimeout(()=>r.abort(),1e4),s=await fetch(n,{method:"POST",body:e,headers:{"Content-Type":"application/x-sentry-envelope"},keepalive:!0,signal:r.signal});if(clearTimeout(i),s.ok)return;let a=new Error(`HTTP ${s.status}`);throw a.status=s.status,a}catch(r){throw r}if(typeof XMLHttpRequest!="undefined")return new Promise((r,i)=>{let s=new XMLHttpRequest;s.open("POST",n,!t),s.setRequestHeader("Content-Type","application/x-sentry-envelope"),s.timeout=1e4,s.onload=()=>{if(s.status>=200&&s.status<300)r();else{let a=new Error(`HTTP ${s.status}`);a.status=s.status,i(a)}},s.onerror=()=>i(new Error("Network error")),s.ontimeout=()=>i(new Error("Request timeout")),s.send(e)});throw new Error("No transport available")}sendSync(e){let t=k(this.dsn);if(navigator.sendBeacon)try{let n=new Blob([e],{type:"application/x-sentry-envelope"});return navigator.sendBeacon(t,n)}catch(n){return!1}if(typeof XMLHttpRequest!="undefined")try{let n=new XMLHttpRequest;return n.open("POST",t,!1),n.setRequestHeader("Content-Type","application/x-sentry-envelope"),n.send(e),n.status>=200&&n.status<300}catch(n){return!1}return!1}sendViaImage(e){try{let t=k(this.dsn),n=new Image,r=encodeURIComponent(btoa(e)),i=t.includes("?")?"&":"?";return n.src=t+i+"sentry_data="+r.substring(0,2e3),!0}catch(t){return!1}}delay(e){return new Promise(t=>setTimeout(t,e))}};var O=class{constructor(e){this.plugins=new Map;this.client=e}add(e){if(!this.plugins.has(e.name)){this.plugins.set(e.name,e);try{e.setup(this.client)}catch(t){console.error(`[LightSDK] Plugin "${e.name}" setup error:`,t)}}}remove(e){let t=this.plugins.get(e);if(t){if(t.destroy)try{t.destroy()}catch(n){console.error(`[LightSDK] Plugin "${e}" destroy error:`,n)}this.plugins.delete(e)}}get(e){return this.plugins.get(e)}has(e){return this.plugins.has(e)}applyBeforeReport(e){let t=e;for(let n of this.plugins.values())if(n.beforeReport)try{let r=n.beforeReport(t);if(r===null)return null;t=r}catch(r){console.error(`[LightSDK] Plugin "${n.name}" beforeReport error:`,r)}return t}applyAfterReport(e){for(let t of this.plugins.values())if(t.afterReport)try{t.afterReport(e)}catch(n){console.error(`[LightSDK] Plugin "${t.name}" afterReport error:`,n)}}destroy(){for(let e of this.plugins.values())if(e.destroy)try{e.destroy()}catch(t){console.error(`[LightSDK] Plugin "${e.name}" destroy error:`,t)}this.plugins.clear()}};var N=class{constructor(e){this.breadcrumbs=[];this.enabled=!0;var t,n,r,i;if(!e.dsn)throw new Error("DSN is required");this.dsn=ee(e.dsn),this.configManager=new M(e),this.eventBus=new B,this.reporter=new _(this.dsn,(t=this.configManager.get("maxRetries"))!=null?t:3,(n=this.configManager.get("retryDelay"))!=null?n:1e3),this.pluginManager=new O(this),this.queue=new I((r=this.configManager.get("maxQueueSize"))!=null?r:100,(i=this.configManager.get("flushInterval"))!=null?i:5e3,this.eventBus,async s=>this.flushEvents(s),s=>this.syncFlushEvents(s)),this.enabled=this.configManager.get("enabled")!==!1}get maxBreadcrumbs(){var e;return(e=this.configManager.get("maxBreadcrumbs"))!=null?e:20}get config(){return this.configManager.getAll()}init(){this.enabled&&(this.queue.start(),this.emit("ready"))}async flushEvents(e){let t=[];for(let i of e){let s=this.pluginManager.applyBeforeReport(i);s&&t.push(s)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let s=this.configManager.applyToEvent(i,n);n=!1;let a=this.configManager.get("beforeSend"),c=a?a(s):s;c&&r.push(c)}if(r.length!==0){await this.reporter.report(r),this.configManager.markContextReported();for(let i of r)this.pluginManager.applyAfterReport(i)}}syncFlushEvents(e){let t=[];for(let i of e){let s=this.pluginManager.applyBeforeReport(i);s&&t.push(s)}if(t.length===0)return;let n=!0,r=[];for(let i of t){let s=this.configManager.applyToEvent(i,n);n=!1;let a=this.configManager.get("beforeSend"),c=a?a(s):s;c&&r.push(c)}r.length!==0&&(this.reporter.reportSync(r),this.configManager.markContextReported())}on(e,t){this.eventBus.on(e,t)}off(e,t){this.eventBus.off(e,t)}emit(e,...t){this.eventBus.emit(e,...t)}captureException(e){if(!this.enabled)return;let t=this.buildErrorEvent(e);this.configManager.isIgnoredError(t.message)||(t.breadcrumbs=[...this.breadcrumbs],this.queue.enqueue(t))}captureMessage(e,t="info"){if(!this.enabled)return;let n={type:"error",level:t,message:e,timestamp:d(),breadcrumbs:[...this.breadcrumbs]};this.configManager.isIgnoredError(e)||this.queue.enqueue(n)}captureEvent(e){if(!this.enabled)return;let t={timestamp:d(),level:"info",breadcrumbs:[...this.breadcrumbs],...e};this.queue.enqueue(t)}buildErrorEvent(e){let t=d();if(e instanceof Error){let r=P(e.stack,{maxFrames:5}),i=L(e.name,e.message,r);return{type:"error",level:"error",message:e.message,timestamp:t,exception:{type:e.name,value:e.message,stacktrace:{frames:r}},fingerprint:i}}return{type:"error",level:"error",message:String(e),timestamp:t}}setUser(e){this.configManager.setUser(e)}setTag(e,t){this.configManager.setTag(e,t)}setTags(e){this.configManager.setTags(e)}setExtra(e,t){this.configManager.setExtra(e,t)}addBreadcrumb(e){let t={...e,timestamp:d()};this.breadcrumbs.push(t),this.breadcrumbs.length>this.maxBreadcrumbs&&this.breadcrumbs.shift()}async flush(){await this.queue.flush()}disable(){this.enabled=!1}enable(){this.enabled=!0}use(e){this.pluginManager.add(e)}destroy(){this.queue.destroy(),this.pluginManager.destroy(),this.eventBus.destroy(),this.breadcrumbs=[]}},q=N;var H=class{constructor(){this.name="error";this.version="1.0.0";this.config={maxStackFrames:5,captureNodeModules:!1,captureColumn:!0,relativePathOnly:!1,resourceSampleRate:.1}}setup(e){this.client=e,this.loadConfig(),this.setupGlobalError(),this.setupUnhandledRejection(),this.setupResourceError()}loadConfig(){let e=this.client.config;e&&y(e.error)&&(this.config={...this.config,...e.error}),e.ignoreErrors&&(this.config.ignoreErrors=[...this.config.ignoreErrors||[],...e.ignoreErrors]),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls]),e.includePaths&&(this.config.includePaths=[...this.config.includePaths||[],...e.includePaths])}shouldIgnoreError(e){return e?T(e,this.config.ignoreErrors||[]):!1}shouldIgnoreScriptUrl(e){return C(e,this.config.ignoreUrls||[])}shouldIncludePath(e){let t=this.config.includePaths||[];return t.length===0?!0:e.some(n=>t.some(r=>{var i;return typeof r=="string"?(i=n.filename)==null?void 0:i.includes(r):r.test(n.filename||"")}))}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:1;return e>=1?!0:e<=0?!1:Math.random()=1?!0:e<=0?!1:Math.random(){try{if(this.originalOnError&&this.originalOnError.call(window,e,t,n,r,i),t&&this.shouldIgnoreScriptUrl(t))return!1;let s=this.buildErrorEvent(i||e,t,n,r);s&&this.client.captureEvent(s)}catch(s){console.error("[LightSDK] ErrorPlugin global error handler error:",s)}return!1})}setupUnhandledRejection(){typeof window!="undefined"&&(this.onUnhandledRejection=e=>{try{let t=e.reason,n=this.extractUrlFromReason(t);if(n&&this.shouldIgnoreScriptUrl(n))return;let r=this.buildErrorEvent(t);r&&(r.type==="error"&&(r.tags={...r.tags||{},unhandled:"true",promise:"true"}),this.client.captureEvent(r))}catch(t){console.error("[LightSDK] ErrorPlugin unhandledrejection handler error:",t)}},window.addEventListener("unhandledrejection",this.onUnhandledRejection))}extractUrlFromReason(e){if(e instanceof Error&&e.stack){let t=e.stack.match(/at\s+(?:.+?\s+)?\((https?:\/\/[^:]+):\d+:\d+\)/);if(t)return t[1];let n=e.stack.match(/(https?:\/\/[^\s)]+):\d+:\d+/);if(n)return n[1]}return null}setupResourceError(){typeof window!="undefined"&&(this.onResourceError=e=>{var i;let t=e.target;if(!t)return;let n=(i=t.tagName)==null?void 0:i.toLowerCase(),r=t.src||t.href;if(n&&r&&["img","script","link","audio","video"].includes(n))try{if(this.shouldIgnoreScriptUrl(r)||!this.shouldSampleResource())return;let s=`resource:${n}`,a=x(r);this.client.captureEvent({type:"error",level:"warning",message:`Resource load failed: ${n} ${a}`,timestamp:d(),fingerprint:s,tags:{resource_type:n,resource_url:a},exception:{type:"ResourceError",value:`Failed to load ${n} resource`}})}catch(s){console.error("[LightSDK] ErrorPlugin resource error handler error:",s)}},window.addEventListener("error",this.onResourceError,!0))}buildErrorEvent(e,t,n,r){let i=d();if(e instanceof Error){if(this.shouldIgnoreError(e.message))return null;let c=P(e.stack,{captureNodeModules:this.config.captureNodeModules,captureColumn:this.config.captureColumn,relativePathOnly:this.config.relativePathOnly,maxFrames:this.config.maxStackFrames});if(!this.shouldIncludePath(c)||!this.shouldSample())return null;let u=L(e.name,e.message,c);return{type:"error",level:"error",message:e.message,timestamp:i,exception:{type:e.name,value:e.message,stacktrace:{frames:c}},fingerprint:u}}let s=String(e);if(this.shouldIgnoreError(s))return null;let a=[];return t&&a.push({filename:t,lineno:n,colno:this.config.captureColumn?r:void 0,in_app:!0}),!this.shouldIncludePath(a)||!this.shouldSample()?null:{type:"error",level:"error",message:s,timestamp:i,exception:{type:"Error",value:s,stacktrace:{frames:a}}}}destroy(){typeof window!="undefined"&&(this.originalOnError!==void 0&&(window.onerror=this.originalOnError),this.onUnhandledRejection&&window.removeEventListener("unhandledrejection",this.onUnhandledRejection),this.onResourceError&&window.removeEventListener("error",this.onResourceError,!0))}},j=H;var $=class{constructor(){this.name="performance";this.version="1.0.0";this.config={sampleRate:.1,captureLongTasks:!0,captureResources:!1,resourceSampleRate:.01,longTaskSampleRate:.05,captureNavigation:!0,captureFP:!0};this.observers=[];this.navLoadTimer=null;this.totalBlockingTime=0;this.fcpTime=null;this.lastLongTaskEndTime=0;this.ttiReported=!1;this.ttiTimer=null}setup(e){this.client=e,this.loadConfig(),this.observeWebVitals(),this.observeLongTasks(),this.observeNavigation(),this.observeResources()}loadConfig(){let e=this.client.config;e&&y(e.performance)&&(this.config={...this.config,...e.performance})}shouldSample(e="default"){var n,r,i;let t=(n=this.config.sampleRate)!=null?n:.1;return e==="resource"?t=(r=this.config.resourceSampleRate)!=null?r:.01:e==="longtask"?t=(i=this.config.longTaskSampleRate)!=null?i:.05:e==="vitals"&&(t=1),t>=1?!0:t<=0?!1:Math.random(){let n=t.getEntriesByName("first-paint");n.length>0&&this.reportMetric("FP",n[0].startTime,"ms",{},"vitals")});e.observe({type:"paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeLCP(){try{let e=new PerformanceObserver(t=>{let n=t.getEntries(),r=n[n.length-1];if(r){let i=r.renderTime||r.loadTime||r.startTime;this.reportMetric("LCP",i,"ms",{},"vitals")}});e.observe({type:"largest-contentful-paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeFID(){try{let e=new PerformanceObserver(t=>{let n=t.getEntries();if(n.length>0){let r=n[0],i=r.processingStart-r.startTime;this.reportMetric("FID",i,"ms",{},"vitals")}});e.observe({type:"first-input",buffered:!0}),this.observers.push(e)}catch(e){}}observeCLS(){try{let e=0,t=0,n=[],r=new PerformanceObserver(i=>{let s=i.getEntries();for(let a of s)if(!a.hadRecentInput){let c=n[0],u=n[n.length-1];t&&a.startTime-((u==null?void 0:u.duration)||0)<1e3&&a.startTime-c.startTime<5e3?(t+=a.value||0,n.push(a)):(t=a.value||0,n=[a]),t>e&&(e=t,this.reportMetric("CLS",e,"",{},"vitals"))}});r.observe({type:"layout-shift",buffered:!0}),this.observers.push(r)}catch(e){}}observeFCP(){try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByName("first-contentful-paint");n.length>0&&(this.fcpTime=n[0].startTime,this.lastLongTaskEndTime=n[0].startTime,this.reportMetric("FCP",n[0].startTime,"ms",{},"vitals"),this.scheduleTTI())});e.observe({type:"paint",buffered:!0}),this.observers.push(e)}catch(e){}}observeTTFB(){try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByType("navigation");if(n.length>0){let r=n[0],i=r.responseStart-r.requestStart;this.reportMetric("TTFB",Math.max(0,i),"ms",{},"vitals")}});e.observe({type:"navigation",buffered:!0}),this.observers.push(e)}catch(e){}}observeLongTasks(){if(this.config.captureLongTasks)try{let e=new PerformanceObserver(t=>{let n=t.getEntries();for(let r of n){let i=r.startTime+r.duration;if(i>this.lastLongTaskEndTime&&(this.lastLongTaskEndTime=i),this.fcpTime!==null&&r.startTime>=this.fcpTime){let s=r.duration-50;s>0&&(this.totalBlockingTime+=s)}this.reportMetric("longtask",r.duration,"ms",{},"longtask"),this.scheduleTTI()}});e.observe({type:"longtask",buffered:!0}),this.observers.push(e)}catch(e){}}scheduleTTI(){this.ttiReported||(this.ttiTimer&&clearTimeout(this.ttiTimer),this.ttiTimer=setTimeout(()=>{if(this.ttiReported)return;this.ttiReported=!0;let e=Math.max(this.lastLongTaskEndTime,this.fcpTime||0);this.reportMetric("TTI",e,"ms",{},"vitals"),this.reportMetric("TBT",this.totalBlockingTime,"ms",{},"vitals")},5e3))}observeNavigation(){this.config.captureNavigation&&typeof performance!="undefined"&&(this.onLoad=()=>{this.navLoadTimer=setTimeout(()=>{let e=performance.timing;if(!e)return;let t=e.navigationStart,n={dom_ready:e.domContentLoadedEventEnd-t,load_time:e.loadEventEnd-t,dns:e.domainLookupEnd-e.domainLookupStart,tcp:e.connectEnd-e.connectStart,ssl:e.secureConnectionStart>0?e.connectEnd-e.secureConnectionStart:0,ttfb:e.responseStart-e.requestStart,download:e.responseEnd-e.responseStart,dom_parse:e.domInteractive-e.responseEnd};for(let[r,i]of Object.entries(n))i>0&&this.reportMetric(r,i,"ms");this.navLoadTimer=null},0)},window.addEventListener("load",this.onLoad))}observeResources(){if(this.config.captureResources&&typeof performance!="undefined")try{let e=new PerformanceObserver(t=>{let n=t.getEntriesByType("resource");for(let r of n)r.duration>1e3&&this.reportMetric("resource_slow",r.duration,"ms",{resource_name:r.name.substring(0,200),resource_type:r.initiatorType},"resource")});e.observe({type:"resource",buffered:!0}),this.observers.push(e)}catch(e){}}getRating(e,t){let r={LCP:{good:2500,poor:4e3},FCP:{good:1800,poor:3e3},FP:{good:1800,poor:3e3},FID:{good:100,poor:300},CLS:{good:.1,poor:.25},TTFB:{good:800,poor:1800}}[e];return!r||t<=r.good?"good":t<=r.poor?"needs-improvement":"poor"}reportMetric(e,t,n,r={},i="default"){if(!this.shouldSample(i))return;let s=this.getRating(e,t),a={type:"performance",level:"info",metric:e,value:Math.round(t*100)/100,unit:n,rating:s,timestamp:d(),tags:{metric:e,rating:s,...r}};this.client.captureEvent(a)}destroy(){for(let e of this.observers)try{e.disconnect()}catch(t){}this.observers=[],this.ttiTimer&&(clearTimeout(this.ttiTimer),this.ttiTimer=null),this.navLoadTimer&&(clearTimeout(this.navLoadTimer),this.navLoadTimer=null),typeof window!="undefined"&&this.onLoad&&window.removeEventListener("load",this.onLoad)}},W=$;var X=class{constructor(){this.name="network";this.version="1.0.0";this.config={captureXHR:!0,captureFetch:!0,captureSuccess:!1,captureBody:!1,captureRequestBody:!1,captureResponseBody:!1,captureRequestHeaders:["content-type","user-agent"],captureResponseHeaders:["content-type","content-length"],errorSampleRate:1,successSampleRate:.01,slowThreshold:3e3,slowSampleRate:1,ignoreStatusCodes:[]}}setup(e){this.client=e,this.loadConfig(),this.config.captureFetch&&this.patchFetch(),this.config.captureXHR&&this.patchXHR()}loadConfig(){let e=this.client.config;e&&y(e.network)&&(this.config={...this.config,...e.network}),e.ignoreUrls&&(this.config.ignoreUrls=[...this.config.ignoreUrls||[],...e.ignoreUrls])}shouldIgnore(e){return C(e,this.config.ignoreUrls||[])}shouldSample(e,t){var r,i,s,a;let n=e?(r=this.config.errorSampleRate)!=null?r:1:(i=this.config.successSampleRate)!=null?i:.01;return!e&&t>=((s=this.config.slowThreshold)!=null?s:3e3)&&(n=(a=this.config.slowSampleRate)!=null?a:1),n>=1?!0:n<=0?!1:Math.random(){var h;let p=d()-r,w=u.ok,f=u.status;return(h=e.config.ignoreStatusCodes)!=null&&h.includes(f)||!e.shouldSample(!w,p)||(!w||e.config.captureSuccess)&&e.reportNetwork({sub_type:"fetch",method:s.toUpperCase(),url:a,status_code:f,duration:p,success:w,error:w?void 0:`HTTP ${f}`}),u},u=>{let p=d()-r;throw e.shouldSample(!0,p)&&e.reportNetwork({sub_type:"fetch",method:s.toUpperCase(),url:a,duration:p,success:!1,error:(u==null?void 0:u.message)||"Network error"}),u}),c}}patchXHR(){if(typeof XMLHttpRequest=="undefined")return;this.originalXHROpen=XMLHttpRequest.prototype.open,this.originalXHRSend=XMLHttpRequest.prototype.send;let e=this;XMLHttpRequest.prototype.open=function(t,n){return this._lightMethod=t,this._lightUrl=typeof n=="string"?n:n.toString(),this._lightTracked=!1,e.originalXHROpen.apply(this,arguments)},XMLHttpRequest.prototype.send=function(t){let n=d(),r=this._lightMethod||"GET",i=this._lightUrl||"",s=this._lightTracked;if(e.shouldIgnore(i)||s)return e.originalXHRSend.apply(this,arguments);this._lightTracked=!0;let a=x(i),c=0;t&&typeof t=="string"&&(c=t.length);let u=this.onloadend;return this.onloadend=function(p){var v;let w=d()-n,f=this.status,h=f>=200&&f<300;if(!((v=e.config.ignoreStatusCodes)!=null&&v.includes(f))&&e.shouldSample(!h,w)&&(!h||e.config.captureSuccess)){let g=0;try{let E=this.getResponseHeader("content-length");E&&(g=parseInt(E,10)),!g&&this.responseText&&(g=this.responseText.length)}catch(E){}e.reportNetwork({sub_type:"xhr",method:r.toUpperCase(),url:a,status_code:f,duration:w,request_size:c||void 0,response_size:g||void 0,success:h,error:h?void 0:`HTTP ${f}`})}if(u)return u.call(this,p)},e.originalXHRSend.apply(this,arguments)}}reportNetwork(e){let t={type:"network",level:e.success?"info":"error",timestamp:d(),tags:{method:e.method,sub_type:e.sub_type,success:String(e.success)},...e};this.client.captureEvent(t)}destroy(){this.originalFetch&&(window.fetch=this.originalFetch),this.originalXHROpen&&(XMLHttpRequest.prototype.open=this.originalXHROpen),this.originalXHRSend&&(XMLHttpRequest.prototype.send=this.originalXHRSend)}},A=X;var K=class{constructor(){this.name="behavior";this.version="1.0.0";this.config={capturePV:!0,captureClick:!0,captureRoute:!0,captureDuration:!0,captureScroll:!0,clickThrottle:300,scrollThrottle:1e3,sampleRate:.01};this.lastClickTime=0;this.lastScrollTime=0;this.maxScrollDepth=0;this.pageEnterTime=0;this.scrollReported=!1}setup(e){this.client=e,this.loadConfig(),this.config.capturePV&&this.trackPV(),this.config.captureClick&&this.trackClick(),this.config.captureRoute&&this.trackRoute(),this.config.captureDuration&&this.trackPageDuration(),this.config.captureScroll&&this.trackScroll()}loadConfig(){let e=this.client.config;e&&y(e.behavior)&&(this.config={...this.config,...e.behavior})}shouldSample(){var t;let e=(t=this.config.sampleRate)!=null?t:.01;return e>=1?!0:e<=0?!1:Math.random(){var a,c,u;let t=d(),n=(a=this.config.clickThrottle)!=null?a:300;if(t-this.lastClickTime{let r=b();if(r!==e){let i=e,s=r;e=r,this.reportBehavior({sub_type:"route",page_url:s,referrer:i,properties:{from:i,to:s}}),this.pageEnterTime=d(),this.maxScrollDepth=0,this.scrollReported=!1}};this.originalPushState=history.pushState,this.originalReplaceState=history.replaceState;let n=this;history.pushState=function(){let r=n.originalPushState.apply(this,arguments);return setTimeout(t,0),r},history.replaceState=function(){let r=n.originalReplaceState.apply(this,arguments);return setTimeout(t,0),r},this.onPopState=()=>{setTimeout(t,0)},this.onHashchange=()=>{setTimeout(t,0)},window.addEventListener("popstate",this.onPopState),window.addEventListener("hashchange",this.onHashchange)}trackPageDuration(){if(typeof window=="undefined")return;this.pageEnterTime=d();let e=()=>{let t=d()-this.pageEnterTime;t>1e3&&(this.config.captureScroll&&!this.scrollReported&&this.reportScroll(),this.reportBehavior({sub_type:"duration",page_url:b(),properties:{duration:t,max_scroll_depth:this.maxScrollDepth}}))};this.onVisibilityChange=()=>{document.hidden?e():this.pageEnterTime=d()},this.onBeforeUnload=e,this.onPagehide=e,document.addEventListener("visibilitychange",this.onVisibilityChange),window.addEventListener("beforeunload",this.onBeforeUnload),window.addEventListener("pagehide",this.onPagehide)}trackScroll(){typeof window!="undefined"&&(this.onScroll=()=>{var r;let e=d(),t=(r=this.config.scrollThrottle)!=null?r:1e3;if(e-this.lastScrollTimethis.maxScrollDepth&&(this.maxScrollDepth=n)},window.addEventListener("scroll",this.onScroll,{passive:!0}))}reportScroll(){this.scrollReported||(this.scrollReported=!0,this.reportBehavior({sub_type:"scroll",page_url:b(),properties:{max_depth:this.maxScrollDepth}}))}reportBehavior(e){if(!this.shouldSample())return;let t={type:"behavior",level:"info",timestamp:d(),tags:{sub_type:e.sub_type,page_url:b()},...e};this.client.captureEvent(t)}destroy(){typeof window!="undefined"&&(this.originalPushState&&(history.pushState=this.originalPushState),this.originalReplaceState&&(history.replaceState=this.originalReplaceState),this.onPopState&&window.removeEventListener("popstate",this.onPopState),this.onHashchange&&window.removeEventListener("hashchange",this.onHashchange),this.onBeforeUnload&&window.removeEventListener("beforeunload",this.onBeforeUnload),this.onPagehide&&window.removeEventListener("pagehide",this.onPagehide),this.onScroll&&window.removeEventListener("scroll",this.onScroll)),typeof document!="undefined"&&(this.onClick&&document.removeEventListener("click",this.onClick,!0),this.onVisibilityChange&&document.removeEventListener("visibilitychange",this.onVisibilityChange))}},V=K;var _e="light-sentry-offline",m="events",z=1e3,Q=class{constructor(){this.name="offline";this.version="1.0.0";this.config={maxEvents:z};this.db=null;this.isOnline=!0;this.isSyncing=!1;this.pendingEvents=[]}setup(e){this.client=e,this.loadConfig(),this.initDatabase(),this.setupNetworkListeners()}loadConfig(){let e=this.client.config;e&&typeof e.offline=="object"&&(this.config={...this.config,...e.offline})}async initDatabase(){if(typeof indexedDB=="undefined"){console.warn("[LightSDK] OfflinePlugin: IndexedDB not available");return}return new Promise((e,t)=>{let n=indexedDB.open(_e,1);n.onerror=()=>{console.warn("[LightSDK] OfflinePlugin: Failed to open IndexedDB"),t(n.error)},n.onsuccess=()=>{this.db=n.result,e()},n.onupgradeneeded=r=>{let i=r.target.result,s=r.oldVersion;i.objectStoreNames.contains(m)||i.createObjectStore(m,{keyPath:"id"}).createIndex("timestamp","timestamp",{unique:!1})}})}setupNetworkListeners(){typeof window!="undefined"&&(this.isOnline=navigator.onLine,this.onOnline=()=>{console.info("[LightSDK] OfflinePlugin: Network online"),this.isOnline=!0,this.syncPendingEvents()},this.onOffline=()=>{console.info("[LightSDK] OfflinePlugin: Network offline"),this.isOnline=!1},window.addEventListener("online",this.onOnline),window.addEventListener("offline",this.onOffline))}generateId(){return`${Date.now()}-${Math.random().toString(36).substring(2,11)}`}async saveToIndexedDB(e){if(!this.db)return;let t=this.config.maxEvents||z,n=await this.getEventCount();return n>=t&&await this.deleteOldestEvents(n-t+1),new Promise((r,i)=>{let a=this.db.transaction([m],"readwrite").objectStore(m),c={id:this.generateId(),event:{...e,offline:!0},timestamp:d()},u=a.add(c);u.onsuccess=()=>r(),u.onerror=()=>i(u.error)})}getEventCount(){return this.db?new Promise((e,t)=>{let i=this.db.transaction([m],"readonly").objectStore(m).count();i.onsuccess=()=>e(i.result),i.onerror=()=>t(i.error)}):Promise.resolve(0)}async deleteOldestEvents(e){if(!(!this.db||e<=0))return new Promise((t,n)=>{let a=this.db.transaction([m],"readwrite").objectStore(m).index("timestamp").openCursor(),c=0;a.onsuccess=u=>{let p=u.target.result;p&&cn(a.error)})}async getAllStoredEvents(){return this.db?new Promise((e,t)=>{let s=this.db.transaction([m],"readonly").objectStore(m).index("timestamp").getAll();s.onsuccess=()=>{let a=s.result||[];a.sort((c,u)=>c.timestamp-u.timestamp),e(a)},s.onerror=()=>t(s.error)}):[]}async clearStoredEvents(){if(this.db)return new Promise((e,t)=>{let i=this.db.transaction([m],"readwrite").objectStore(m).clear();i.onsuccess=()=>e(),i.onerror=()=>t(i.error)})}async syncPendingEvents(){if(!(!this.isOnline||this.isSyncing)){this.isSyncing=!0,console.info("[LightSDK] OfflinePlugin: Syncing pending events");try{let e=0;for(;this.pendingEvents.length>0;){let n=this.pendingEvents.shift();try{this.client.captureEvent(n),e++}catch(r){console.error("[LightSDK] OfflinePlugin: Failed to re-enqueue memory event",r)}}let t=await this.getAllStoredEvents();if(t.length>0)for(let n of t)try{let r=n.event;this.client.captureEvent(r),e++}catch(r){console.error("[LightSDK] OfflinePlugin: Failed to re-enqueue event",r)}try{await this.client.flush()}catch(n){}await this.clearStoredEvents(),console.info(`[LightSDK] OfflinePlugin: Synced ${e} events`)}catch(e){console.error("[LightSDK] OfflinePlugin: Sync failed",e)}finally{this.isSyncing=!1}}}beforeReport(e){if(!this.isOnline){let n=this.config.maxEvents||z;return this.pendingEvents.length>=n&&this.pendingEvents.shift(),this.pendingEvents.push(e),this.savePendingEventsToDB(),null}let t={...e};return delete t.offline,t}savePendingEventsToDB(){if(this.pendingEvents.length===0)return;let e=this.pendingEvents.splice(0,this.pendingEvents.length),t=0,n=()=>{if(t>=e.length)return;let r=e[t];this.saveToIndexedDB(r).then(()=>{t++,n()}).catch(i=>{console.error("[LightSDK] OfflinePlugin: Failed to save event offline",i),this.pendingEvents.unshift(...e.slice(t))})};n()}destroy(){typeof window!="undefined"&&(this.onOnline&&window.removeEventListener("online",this.onOnline),this.onOffline&&window.removeEventListener("offline",this.onOffline)),this.db&&(this.db.close(),this.db=null)}},G=Q;var J=class{constructor(){this.name="sampling";this.version="1.0.0";this.config={rates:{error:1,performance:1,network:1,behavior:1}}}setup(e){this.client=e,this.loadConfig()}loadConfig(){let e=this.client.config;if(e&&y(e.sampling)){let t=e.sampling;"rates"in t&&y(t.rates)&&(this.config.rates={...this.config.rates,...t.rates})}}shouldSample(e){var r;let t=(r=this.client.config.sampleRate)!=null?r:1;if(t<=0)return!1;let n=this.getTypeRate(e);return t>=1?this.randomCheck(n):this.randomCheck(t*n)}getTypeRate(e){let t=this.config.rates||{},n=e.type;if(n==="count")return 1;let r=t[n];return r!==void 0?r:1}randomCheck(e){return e>=1?!0:e<=0?!1:Math.random()typeof i=="string"))||[];(n.includes("performance")||n.includes("all"))&&t.push(new W),(n.includes("network")||n.includes("all"))&&t.push(new A),(n.includes("behavior")||n.includes("all"))&&t.push(new V);for(let i of t)e.use(i);if(o.plugins)for(let i of o.plugins)typeof i=="object"&&i!==null&&"name"in i&&"setup"in i&&e.use(i);return e.init(),l=e,e}function ce(){return l}function ue(o){l==null||l.captureException(o)}function le(o,e){l==null||l.captureMessage(o,e)}function de(o){l==null||l.captureEvent(o)}function pe(o){l==null||l.setUser(o)}function he(o,e){l==null||l.setTag(o,e)}function fe(o){l==null||l.setTags(o)}function ge(o,e){l==null||l.setExtra(o,e)}function me(o){l==null||l.addBreadcrumb(o)}async function ve(){await(l==null?void 0:l.flush())}function ye(){l==null||l.disable()}function be(){l==null||l.enable()}var Oe={init:ae,getClient:ce,captureException:ue,captureMessage:le,captureEvent:de,setUser:pe,setTag:he,setTags:fe,setExtra:ge,addBreadcrumb:me,flush:ve,disable:ye,enable:be};return ke(De);})();
diff --git a/dist/plugins/BehaviorPlugin.d.ts b/dist/plugins/BehaviorPlugin.d.ts
index 5fb85b0..be9c5f6 100644
--- a/dist/plugins/BehaviorPlugin.d.ts
+++ b/dist/plugins/BehaviorPlugin.d.ts
@@ -9,6 +9,15 @@ declare class BehaviorPlugin implements LightPlugin {
private maxScrollDepth;
private pageEnterTime;
private scrollReported;
+ private originalPushState?;
+ private originalReplaceState?;
+ private onScroll?;
+ private onClick?;
+ private onVisibilityChange?;
+ private onBeforeUnload?;
+ private onPagehide?;
+ private onPopState?;
+ private onHashchange?;
setup(client: LightClient): void;
private loadConfig;
private shouldSample;
diff --git a/dist/plugins/ErrorPlugin.d.ts b/dist/plugins/ErrorPlugin.d.ts
index f253909..cad8eba 100644
--- a/dist/plugins/ErrorPlugin.d.ts
+++ b/dist/plugins/ErrorPlugin.d.ts
@@ -4,15 +4,21 @@ declare class ErrorPlugin implements LightPlugin {
version: string;
private client;
private config;
+ private originalOnError?;
+ private onUnhandledRejection?;
+ private onResourceError?;
setup(client: LightClient): void;
private loadConfig;
private shouldIgnoreError;
private shouldIgnoreScriptUrl;
+ private shouldIncludePath;
+ private shouldSample;
+ private shouldSampleResource;
private setupGlobalError;
private setupUnhandledRejection;
+ private extractUrlFromReason;
private setupResourceError;
private buildErrorEvent;
- private parseStackTrace;
destroy(): void;
}
export default ErrorPlugin;
diff --git a/dist/plugins/OfflinePlugin.d.ts b/dist/plugins/OfflinePlugin.d.ts
index 09b8e22..430d9b2 100644
--- a/dist/plugins/OfflinePlugin.d.ts
+++ b/dist/plugins/OfflinePlugin.d.ts
@@ -7,6 +7,8 @@ declare class OfflinePlugin implements LightPlugin {
private db;
private isOnline;
private isSyncing;
+ private onOnline?;
+ private onOffline?;
private pendingEvents;
setup(client: LightClient): void;
private loadConfig;
@@ -14,11 +16,13 @@ declare class OfflinePlugin implements LightPlugin {
private setupNetworkListeners;
private generateId;
private saveToIndexedDB;
+ private getEventCount;
private deleteOldestEvents;
private getAllStoredEvents;
private clearStoredEvents;
syncPendingEvents(): Promise;
beforeReport(event: SentryEvent): SentryEvent | null;
+ private savePendingEventsToDB;
destroy(): void;
}
export default OfflinePlugin;
diff --git a/dist/plugins/PerformancePlugin.d.ts b/dist/plugins/PerformancePlugin.d.ts
index 6f0d0de..9b41493 100644
--- a/dist/plugins/PerformancePlugin.d.ts
+++ b/dist/plugins/PerformancePlugin.d.ts
@@ -4,6 +4,14 @@ declare class PerformancePlugin implements LightPlugin {
version: string;
private client;
private config;
+ private observers;
+ private onLoad?;
+ private navLoadTimer;
+ private totalBlockingTime;
+ private fcpTime;
+ private lastLongTaskEndTime;
+ private ttiReported;
+ private ttiTimer;
setup(client: LightClient): void;
private loadConfig;
private shouldSample;
@@ -15,6 +23,7 @@ declare class PerformancePlugin implements LightPlugin {
private observeFCP;
private observeTTFB;
private observeLongTasks;
+ private scheduleTTI;
private observeNavigation;
private observeResources;
private getRating;
diff --git a/dist/plugins/SamplingPlugin.d.ts b/dist/plugins/SamplingPlugin.d.ts
new file mode 100644
index 0000000..ae21911
--- /dev/null
+++ b/dist/plugins/SamplingPlugin.d.ts
@@ -0,0 +1,15 @@
+import type { LightPlugin, LightClient, SentryEvent } from '../types';
+declare class SamplingPlugin implements LightPlugin {
+ name: string;
+ version: string;
+ private client;
+ private config;
+ setup(client: LightClient): void;
+ private loadConfig;
+ private shouldSample;
+ private getTypeRate;
+ private randomCheck;
+ beforeReport(event: SentryEvent): SentryEvent | null;
+ destroy(): void;
+}
+export default SamplingPlugin;
diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts
index 09d825c..419b5b9 100644
--- a/dist/types/index.d.ts
+++ b/dist/types/index.d.ts
@@ -1,4 +1,9 @@
export type EventLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug';
+export interface ContextLevelConfig {
+ maxStackFrames: number;
+ maxBreadcrumbs: number;
+}
+export type ContextLevelMap = Record;
export interface UserInfo {
id?: string;
username?: string;
@@ -113,6 +118,15 @@ export interface DSNInfo {
host: string;
projectId: string;
}
+export interface SamplingConfig {
+ rates?: {
+ error?: number;
+ performance?: number;
+ network?: number;
+ behavior?: number;
+ [key: string]: number | undefined;
+ };
+}
export interface LightConfig {
dsn: string;
release?: string;
@@ -126,9 +140,12 @@ export interface LightConfig {
ignoreErrors?: (string | RegExp)[];
ignoreUrls?: (string | RegExp)[];
includePaths?: (string | RegExp)[];
+ maxBreadcrumbs?: number;
beforeSend?: (event: SentryEvent) => SentryEvent | null;
user?: UserInfo;
plugins?: (LightPlugin | string)[];
+ contextLevel?: Partial;
+ sampling?: SamplingConfig;
[pluginName: string]: unknown;
}
export interface LightPlugin {
@@ -158,4 +175,7 @@ export interface LightClient {
flush(): Promise;
disable(): void;
enable(): void;
+ use(plugin: LightPlugin): void;
+ init(): void;
+ destroy(): void;
}
diff --git a/dist/utils/helper.d.ts b/dist/utils/helper.d.ts
index 792d994..6d8541f 100644
--- a/dist/utils/helper.d.ts
+++ b/dist/utils/helper.d.ts
@@ -6,3 +6,4 @@ export declare function isString(value: unknown): value is string;
export declare function isObject(value: unknown): value is Record;
export declare function safeGet(fn: () => T, defaultValue: T): T;
export declare function truncate(str: string, maxLen: number): string;
+export declare function matchPatterns(value: string, patterns: (string | RegExp)[]): boolean;
diff --git a/dist/utils/stacktrace.d.ts b/dist/utils/stacktrace.d.ts
new file mode 100644
index 0000000..b8878b7
--- /dev/null
+++ b/dist/utils/stacktrace.d.ts
@@ -0,0 +1,8 @@
+import type { StackFrame } from '../types';
+export interface ParseStackOptions {
+ captureNodeModules?: boolean;
+ captureColumn?: boolean;
+ relativePathOnly?: boolean;
+ maxFrames?: number;
+}
+export declare function parseStackTrace(stack?: string, options?: ParseStackOptions): StackFrame[];
diff --git a/docs/00-architecture.md b/docs/00-architecture.md
new file mode 100644
index 0000000..e9e5a50
--- /dev/null
+++ b/docs/00-architecture.md
@@ -0,0 +1,227 @@
+# Light-Sentry 轻量级前端监控系统 - 架构设计文档
+
+## 一、设计理念
+
+### 1.1 核心原则
+
+- **轻量优先**:SDK < 10KB (gzip),服务端单节点可运行
+- **插件化架构**:核心极小,功能通过插件扩展,按需加载
+- **性能友好**:不阻塞主线程,不影响页面性能
+- **易于部署**:docker-compose up 一键启动,无需复杂运维
+- **兼容 Sentry**:接口兼容 Sentry 协议,可平滑迁移
+
+### 1.2 设计目标
+
+| 指标 | 目标值 | 说明 |
+|------|--------|------|
+| SDK 核心体积 | < 5KB gzip | 只含事件总线和上报 |
+| SDK 完整体积 | < 15KB gzip | 含错误+性能+网络插件 |
+| 单节点承载 | 10万+ 事件/天 | 1C2G 服务器 |
+| 部署时间 | < 5 分钟 | docker-compose 一键部署 |
+| 数据延迟 | < 30 秒 | 从上报到可查询 |
+
+---
+
+## 二、整体架构
+
+### 2.1 架构图
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 前端 SDK │
+│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────────┐ │
+│ │ Error │ │ Perf │ │ Network │ │ Behavior │ │
+│ │ Plugin │ │ Plugin │ │ Plugin │ │ Plugin │ │
+│ └────┬────┘ └────┬────┘ └────┬────┘ └──────┬───────┘ │
+│ └─────────────┴─────────────┴──────────────┘ │
+│ │ │
+│ ┌─────┴─────┐ │
+│ │ Core │ │
+│ │ (EventBus │ │
+│ │ + Queue │ │
+│ │ + Report)│ │
+│ └─────┬─────┘ │
+└──────────────────────────┼───────────────────────────────────┘
+ │ HTTP (beacon / fetch / img)
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ Nginx 反向代理 │
+│ 限流 · CORS · 负载均衡 · SSL 卸载 │
+└──────────────────────────┬───────────────────────────────────┘
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ API 接入层 (Node.js) │
+│ ┌───────────────────────────────────────────────────────┐ │
+│ │ 1. DSN 认证 (publicKey 校验) │ │
+│ │ 2. 协议解析 (envelope / store / 自定义) │ │
+│ │ 3. 数据清洗 (脱敏、过滤、去重) │ │
+│ │ 4. 错误指纹计算 │ │
+│ │ 5. 内存队列 + 批量写入 │ │
+│ └───────────────────────────────────────────────────────┘ │
+└──────────┬───────────────────────────┬──────────────────────┘
+ ▼ ▼
+┌─────────────────────┐ ┌───────────────────────┐
+│ Loki (日志) │ │ MySQL (聚合数据) │
+│ - 原始错误堆栈 │ │ - 项目配置 │
+│ - 性能指标明细 │ │ - 错误聚合统计 │
+│ - 用户行为日志 │ │ - 性能指标趋势 │
+│ 保留: 7-30 天 │ │ - 告警规则 │
+└─────────────────────┘ │ - 用户管理 │
+ └───────────┬───────────┘
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ 查询 & 展示层 │
+│ ┌──────────────┐ ┌──────────┐ ┌──────────────────────┐ │
+│ │ 管理后台 │ │ 告警引擎 │ │ Grafana 仪表盘 │ │
+│ │ (原生 JS) │ │ (规则引擎)│ │ (复用现有工具) │ │
+│ │ - 项目管理 │ │ - 规则 │ │ - 错误趋势图 │ │
+│ │ - 错误查询 │ │ - 收敛 │ │ - 性能仪表盘 │ │
+│ │ - 性能分析 │ │ - 通知 │ │ - 自定义面板 │ │
+│ └──────────────┘ └──────────┘ └──────────────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 模块职责
+
+| 层级 | 模块 | 职责 | 技术选型 |
+|------|------|------|----------|
+| 采集层 | 前端 SDK | 数据采集、队列、上报 | TypeScript + 插件化 |
+| 接入层 | API Server | 认证、解析、清洗、入队 | Node.js + Express |
+| 存储层 | Loki | 原始日志存储 | Grafana Loki |
+| 存储层 | MySQL | 聚合数据、配置、用户 | MySQL 8.0 / SQLite |
+| 计算层 | 聚合服务 | 错误聚合、性能统计 | 定时任务 / 实时聚合 |
+| 计算层 | 告警引擎 | 规则匹配、告警通知 | 内置规则引擎 |
+| 展示层 | 管理后台 | 项目管理、数据查询 | 原生 HTML + JS |
+| 展示层 | Grafana | 数据可视化 | Grafana |
+
+---
+
+## 三、数据流
+
+### 3.1 错误上报流程
+
+```
+前端触发错误
+ ↓
+ErrorPlugin 捕获
+ ↓
+封装为标准事件格式
+ ↓
+加入内存队列
+ ↓
+批量/定时/页面隐藏 触发上报
+ ↓
+sendBeacon / fetch / img 标签
+ ↓
+Nginx (限流 + CORS)
+ ↓
+API Server
+ ├─ DSN 认证
+ ├─ envelope 解析
+ ├─ 数据清洗 (脱敏IP、UA)
+ ├─ 计算错误指纹
+ └─ 写入内存队列
+ ↓
+批量消费
+ ├─ 写入 Loki (原始数据)
+ └─ 更新 MySQL 聚合表
+ ↓
+Grafana / 管理后台 查询
+ ↓
+告警引擎 检测规则
+ ↓
+触发通知 (Webhook / 邮件)
+```
+
+### 3.2 性能指标流程
+
+```
+PerformancePlugin 监听性能指标
+ ↓
+(FP / FCP / LCP / CLS / TTI / Long Task)
+ ↓
+采样过滤 (默认 10% 采样率)
+ ↓
+批量上报
+ ↓
+API Server 接收
+ ├─ 数据校验
+ └─ 写入 Loki
+ ↓
+定时聚合任务 (每 5 分钟)
+ ├─ 计算 P50 / P90 / P95 / P99
+ ├─ 按项目 + 页面 + 小时 聚合
+ └─ 写入 MySQL 性能表
+ ↓
+Grafana 展示趋势图
+```
+
+---
+
+## 四、技术选型说明
+
+### 4.1 为什么用 Loki 而不是 ES/ClickHouse?
+
+| 特性 | Loki | Elasticsearch | ClickHouse |
+|------|------|---------------|------------|
+| 部署复杂度 | 极低(单二进制) | 高(需调优) | 中 |
+| 资源消耗 | 低(内存 + 磁盘) | 高(内存密集) | 中高 |
+| 查询性能 | 中等(日志足够) | 高 | 极高 |
+| 全文检索 | 支持(Label + grep) | 强大 | 支持 |
+| Grafana 集成 | 原生 | 需配置 | 需配置 |
+| 适合场景 | 日志、错误追踪 | 全文检索、复杂分析 | OLAP、大数据量 |
+
+**结论**:轻量级监控,Loki 足够用,运维成本最低,和 Grafana 集成最好。
+
+### 4.2 为什么用 MySQL 存聚合数据?
+
+- 成熟稳定,运维简单
+- 聚合后数据量不大(每天几千~几万行)
+- 事务支持,数据一致性好
+- 管理后台查询方便
+- 小项目甚至可以用 SQLite,零运维
+
+### 4.3 为什么前端用原生 JS?
+
+- 后台页面不复杂,原生 JS 足够
+- 无构建流程,修改即生效
+- 体积小,加载快
+- 不依赖前端框架生态,长期维护成本低
+
+---
+
+## 五、与现有 Light-Sentry 的关系
+
+### 5.1 现有能力
+
+- ✅ Sentry 兼容接口(store / envelope)
+- ✅ Loki 原始日志存储
+- ✅ Grafana 仪表盘
+- ✅ 项目管理后台
+- ✅ Docker Compose 一键部署
+- ✅ NEL (Network Error Logging) 支持
+
+### 5.2 待完善能力
+
+- 🚧 自研轻量 SDK
+- 🚧 错误指纹聚合
+- 🚧 性能指标聚合
+- 🚧 告警引擎
+- 🚧 源 map 解析
+- 🚧 用户会话追踪
+- 🚧 行为分析插件
+
+---
+
+## 六、文档目录
+
+```
+docs/
+├── 00-architecture.md # 本文档 - 整体架构
+├── 01-sdk-design.md # 前端 SDK 设计
+├── 02-api-server.md # 服务端接入层设计
+├── 03-data-aggregation.md # 数据聚合与存储设计
+├── 04-alert-engine.md # 告警引擎设计
+├── 05-admin-dashboard.md # 管理后台与可视化设计
+└── 06-deployment.md # 部署与运维设计
+```
diff --git a/docs/01-sdk-design.md b/docs/01-sdk-design.md
new file mode 100644
index 0000000..3b31e8a
--- /dev/null
+++ b/docs/01-sdk-design.md
@@ -0,0 +1,1245 @@
+# 前端 SDK 设计文档
+
+## 一、设计原则
+
+### 1.1 核心原则
+
+- **极致轻量**:核心 < 5KB gzip,完整功能 < 15KB gzip
+- **零侵入**:不影响业务代码,不修改原生对象原型
+- **高性能**:所有耗时操作异步化,不阻塞主线程
+- **插件化**:功能模块化,按需加载,可扩展
+- **容错性**:SDK 自身错误不影响页面运行
+- **兼容 Sentry**:DSN 格式、事件格式、上报协议兼容
+
+### 1.2 体积预算
+
+| 模块 | 体积 (gzip) | 说明 |
+|------|-------------|------|
+| Core 核心 | ~3KB | 事件总线、队列、上报、工具函数 |
+| Error 插件 | ~3KB | JS 错误、Promise、资源错误 |
+| Performance 插件 | ~4KB | Web Vitals、长任务、资源时序 |
+| Network 插件 | ~2KB | fetch / xhr 监控 |
+| Behavior 插件 | ~2KB | PV、点击、路由 |
+| **完整 SDK** | **~12KB** | 核心 + 所有插件 |
+
+---
+
+## 二、核心架构
+
+### 2.1 整体结构
+
+```
+┌─────────────────────────────────────────────────┐
+│ LightSDK │
+│ ┌───────────────────────────────────────────┐ │
+│ │ Core (核心) │ │
+│ │ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │
+│ │ │ Config │ │ EventBus │ │ Queue │ │ │
+│ │ │ 配置管理 │ │ 事件总线 │ │ 事件队列 │ │ │
+│ │ └──────────┘ └──────────┘ └────┬────┘ │ │
+│ │ │ │ │
+│ │ ┌──────────┐ ┌──────────┐ │ │ │
+│ │ │ Reporter │ │ Plugins │◄──────┘ │ │
+│ │ │ 上报器 │ │ 插件系统 │ │ │
+│ │ └──────────┘ └──────────┘ │ │
+│ └───────────────────────────────────────────┘ │
+│ ▲ ▲ │
+└─────────┼───────────────────┼───────────────────┘
+ │ │
+ ┌─────┴─────┐ ┌──────┴──────┐
+ │ Plugins │ │ Hooks │
+ │ (插件) │ │ (生命周期) │
+ └───────────┘ └─────────────┘
+```
+
+### 2.2 插件系统
+
+每个插件实现标准接口:
+
+```typescript
+interface LightPlugin {
+ name: string; // 插件唯一标识
+ version: string; // 插件版本
+
+ setup(client: LightClient): void; // 安装时调用
+ destroy?(): void; // 销毁时调用
+
+ // 可选钩子
+ beforeReport?(event: Event): Event | null; // 上报前拦截
+ afterReport?(event: Event): void; // 上报后回调
+}
+```
+
+内置插件列表:
+
+| 插件名 | 功能 | 体积 | 默认开启 |
+|--------|------|------|----------|
+| `error` | JS 错误、Promise 异常、资源加载错误 | ~3KB | 是 |
+| `performance` | Web Vitals、长任务、性能指标 | ~4KB | 否 |
+| `network` | fetch / xhr 请求监控 | ~2KB | 否 |
+| `behavior` | PV、点击、路由、用户行为 | ~2KB | 否 |
+| `sampling` | 采样控制、流量控制 | ~1KB | 是 |
+| `offline` | 离线缓存、网络恢复后补发 | ~2KB | 否 |
+| `breadcrumbs` | 用户行为面包屑 | ~2KB | 否 |
+
+---
+
+## 三、核心模块详细设计
+
+### 3.1 Config - 配置管理
+
+#### 配置项
+
+```typescript
+interface LightConfig {
+ // ===== 基础配置 =====
+ dsn: string; // DSN: https://{publicKey}@{host}/{projectId}
+ release?: string; // 版本号,如 1.0.0
+ environment?: string; // 环境: production / development / staging
+ enabled?: boolean; // 是否启用,默认 true
+
+ // ===== 上报配置 =====
+ sampleRate?: number; // 全局采样率 0-1,默认 1
+ maxQueueSize?: number; // 队列最大长度,默认 100
+ flushInterval?: number; // 定时上报间隔(ms),默认 5000
+ maxRetries?: number; // 失败重试次数,默认 3
+ retryDelay?: number; // 重试基础延迟(ms),默认 1000
+
+ // ===== 数据配置 =====
+ ignoreErrors?: (string | RegExp)[]; // 忽略的错误
+ ignoreUrls?: (string | RegExp)[]; // 忽略的 URL
+ includePaths?: (string | RegExp)[]; // 只收集的路径
+ beforeSend?: (event: Event) => Event | null; // 发送前回调
+
+ // ===== 用户配置 =====
+ user?: {
+ id?: string;
+ username?: string;
+ email?: string;
+ [key: string]: any;
+ };
+
+ // ===== 插件配置 =====
+ plugins?: (LightPlugin | string)[]; // 插件列表
+ [pluginName: string]: any; // 各插件自定义配置
+}
+```
+
+#### DSN 解析
+
+格式:`{protocol}://{publicKey}@{host}/{projectId}`
+
+```
+https://proj_abc123@log.example.com/1001
+ │ │ │ └── projectId: 1001
+ │ │ └────────────────── host: log.example.com
+ │ └────────────────────────────── publicKey: proj_abc123
+ └──────────────────────────────────────── protocol: https
+```
+
+解析后:
+- `projectId`: 项目 ID(纯数字,兼容 Sentry)
+- `publicKey`: 认证公钥(项目内部 ID)
+- `host`: 上报域名
+- `protocol`: http / https
+
+### 3.2 EventBus - 事件总线
+
+简单的发布订阅模式,用于插件间通信:
+
+```typescript
+class EventBus {
+ private handlers: Map = new Map();
+
+ on(event: string, handler: Function): void; // 订阅
+ off(event: string, handler: Function): void; // 取消订阅
+ emit(event: string, data?: any): void; // 发布
+ once(event: string, handler: Function): void; // 一次性订阅
+}
+```
+
+内置事件:
+
+| 事件名 | 触发时机 | 数据 |
+|--------|----------|------|
+| `event` | 新事件产生 | Event 对象 |
+| `report` | 事件上报前 | Event[] 数组 |
+| `reported` | 上报成功 | Event[] 数组 |
+| `error` | SDK 自身错误 | Error |
+| `ready` | SDK 初始化完成 | - |
+
+### 3.3 Queue - 事件队列
+
+#### 队列策略
+
+```
+新事件 → 入队 → 检查是否触发上报 → 是 → 上报
+ │
+ └─ 否 → 等待
+```
+
+触发上报的条件:
+1. **队列满**:达到 `maxQueueSize` 阈值
+2. **定时**:每 `flushInterval` 毫秒
+3. **页面隐藏**:`visibilitychange` → hidden
+4. **页面卸载**:`beforeunload` / `pagehide`
+5. **手动调用**:`client.flush()`
+
+#### 事件去重
+
+- 同一错误(相同指纹)1 分钟内最多上报 3 次
+- 防止死循环导致的事件风暴
+
+### 3.4 Reporter - 上报器
+
+#### 上报策略(优先级从高到低)
+
+| 方式 | 适用场景 | 优点 | 缺点 |
+|------|----------|------|------|
+| `navigator.sendBeacon` | 页面卸载、隐藏 | 异步、不阻塞、可靠 | 仅 POST、大小限制 64KB |
+| `fetch + keepalive` | 现代浏览器 | 灵活、支持自定义 header | 不支持跨域某些场景 |
+| `XMLHttpRequest` | 兼容旧浏览器 | 兼容性好 | 同步会阻塞 |
+| `Image 标签` | 极端兜底 | 兼容性最好 | GET 请求、大小有限 |
+
+#### 上报重试
+
+- 失败后指数退避重试:1s → 2s → 4s
+- 最多重试 `maxRetries` 次
+- 4xx 错误不重试(客户端错误)
+- 5xx / 网络错误才重试
+
+#### 上报格式(兼容 Sentry Envelope)
+
+```
+{"event_id":"abc123","sent_at":"2024-01-01T00:00:00Z"}
+{"type":"event","length":123}
+{"level":"error","message":"...","exception":{...}}
+{"type":"transaction","length":456}
+{"transaction":"...","spans":[...]}
+```
+
+轻量模式下可用简化格式:
+```json
+{
+ "events": [
+ { "type": "error", "data": {...} },
+ { "type": "performance", "data": {...} }
+ ]
+}
+```
+
+---
+
+## 四、内置插件详细设计
+
+### 4.1 Error 插件(错误采集)
+
+#### 采集内容
+
+| 错误类型 | 监听方式 | 说明 |
+|----------|----------|------|
+| JS 运行时错误 | `window.onerror` | 普通 JS 错误 |
+| Promise 异常 | `unhandledrejection` | Promise 未捕获 |
+| 资源加载错误 | `error` 事件捕获阶段 | img、script、link 等 |
+| iframe 错误 | 冒泡 + postMessage | 同域 iframe |
+| 框架错误 | 钩子函数(Vue/React) | 需手动接入 |
+
+#### 错误事件格式
+
+```typescript
+interface ErrorEvent {
+ type: 'error';
+ level: 'fatal' | 'error' | 'warning' | 'info' | 'debug';
+ message: string; // 错误消息
+ exception?: {
+ type: string; // 错误类型: TypeError, ReferenceError...
+ value: string; // 错误消息
+ stacktrace?: {
+ frames: StackFrame[]; // 堆栈帧,倒序(最近的调用在前)
+ };
+ };
+ timestamp: number; // 时间戳
+ release?: string; // 版本
+ environment?: string; // 环境
+ user?: UserInfo; // 用户信息
+ tags?: Record; // 标签
+ extra?: Record; // 额外数据
+ breadcrumbs?: Breadcrumb[]; // 面包屑
+ request?: { // 请求信息
+ url: string;
+ method?: string;
+ headers?: Record;
+ };
+}
+
+interface StackFrame {
+ filename: string; // 文件名
+ function?: string; // 函数名
+ lineno?: number; // 行号
+ colno?: number; // 列号
+ in_app?: boolean; // 是否业务代码
+}
+```
+
+#### 错误指纹
+
+用于错误去重和聚合:
+```
+指纹 = md5(错误类型 + 错误消息(脱敏后) + 堆栈前3帧的文件名+行号)
+```
+
+### 4.2 Performance 插件(性能采集)
+
+#### 采集指标
+
+| 指标 | 全称 | 说明 | 采集方式 |
+|------|------|------|----------|
+| FP | First Paint | 首次绘制 | PerformanceObserver |
+| FCP | First Contentful Paint | 首次内容绘制 | PerformanceObserver |
+| LCP | Largest Contentful Paint | 最大内容绘制 | PerformanceObserver |
+| CLS | Cumulative Layout Shift | 累计布局偏移 | PerformanceObserver |
+| FID | First Input Delay | 首次输入延迟 | PerformanceObserver |
+| TTI | Time to Interactive | 可交互时间 | 长任务 + 网络静默 |
+| TBT | Total Blocking Time | 总阻塞时间 | 长任务累加 |
+| Long Tasks | - | 长任务列表 | PerformanceObserver |
+| Navigation Timing | - | 页面加载各阶段 | performance.timing |
+| Resource Timing | - | 资源加载时序 | performance.getEntriesByType |
+
+#### 性能事件格式
+
+```typescript
+interface PerformanceEvent {
+ type: 'performance';
+ metric: string; // 指标名: lcp, fcp, cls...
+ value: number; // 指标值
+ unit: string; // 单位: ms, score...
+ rating: 'good' | 'needs-improvement' | 'poor'; // 评级
+ timestamp: number;
+ release?: string;
+ environment?: string;
+ tags?: {
+ page_url: string; // 页面 URL
+ route?: string; // 路由路径
+ [key: string]: string;
+ };
+ extra?: Record;
+}
+```
+
+#### 性能评级标准(Google Web Vitals)
+
+| 指标 | Good | Needs Improvement | Poor |
+|------|------|-------------------|------|
+| LCP | < 2.5s | 2.5s - 4.0s | > 4.0s |
+| FID | < 100ms | 100ms - 300ms | > 300ms |
+| CLS | < 0.1 | 0.1 - 0.25 | > 0.25 |
+
+#### 采样策略
+
+- 错误事件:100% 采样
+- 性能指标:默认 10% 采样,可配置
+- 长任务:默认 5% 采样
+- 资源时序:默认 1% 采样
+
+### 4.3 Network 插件(网络监控)
+
+#### 采集内容
+
+- **fetch**:通过 monkey patch 包装 `window.fetch`
+- **XMLHttpRequest**:通过 monkey patch 包装 `XHR.prototype.open/send`
+
+#### 采集字段
+
+```typescript
+interface NetworkEvent {
+ type: 'network';
+ sub_type: 'fetch' | 'xhr';
+ method: string; // 请求方法
+ url: string; // 请求 URL
+ status_code?: number; // 状态码
+ duration?: number; // 耗时(ms)
+ request_size?: number; // 请求大小
+ response_size?: number; // 响应大小
+ success: boolean; // 是否成功
+ error?: string; // 错误信息
+ timestamp: number;
+ release?: string;
+ tags?: {
+ route?: string;
+ [key: string]: string;
+ };
+}
+```
+
+#### 脱敏处理
+
+- URL 中的敏感参数自动脱敏(password、token、key 等)
+- 请求/响应 body 默认不上报(避免数据泄漏)
+- 可配置白名单上报特定接口的 body
+
+### 4.4 Behavior 插件(行为采集)
+
+#### 采集内容
+
+| 行为 | 采集方式 | 说明 |
+|------|----------|------|
+| PV / UV | 页面加载 + 路由变化 | 页面浏览量 |
+| 点击事件 | `click` 事件委托 | 按钮、链接点击 |
+| 路由变化 | History / Hash 变化 | SPA 路由跳转 |
+| 停留时长 | visibilitychange + 计时 | 页面停留时间 |
+| 滚动深度 | scroll 事件 + 防抖 | 页面滚动百分比 |
+
+#### 行为事件格式
+
+```typescript
+interface BehaviorEvent {
+ type: 'behavior';
+ sub_type: 'pv' | 'click' | 'route' | 'scroll' | 'duration';
+ page_url: string;
+ referrer?: string;
+ timestamp: number;
+ properties?: Record; // 各子类型的属性
+}
+```
+
+#### 点击事件属性
+
+```
+- element: 元素标签名 + id + class(如 button#submit.btn-primary)
+- text: 元素文本(截断前 50 字符)
+- xpath: 元素 XPath(可选,默认关)
+```
+
+### 4.5 Sampling 插件(采样控制)
+
+#### 采样维度
+
+- **全局采样率**:所有事件的总采样率
+- **按事件类型采样**:错误 100%,性能 10%,行为 1%
+- **按用户采样**:按 user.id 哈希,稳定采样同一用户
+- **按页面采样**:重要页面 100%,普通页面 10%
+- **错误突增降级**:错误率过高时自动降采样
+
+#### 采样算法
+
+```javascript
+// 基于用户 ID 的稳定采样
+function shouldSample(userId, rate) {
+ const hash = murmurhash(userId || 'anonymous');
+ return (hash % 10000) < (rate * 10000);
+}
+```
+
+### 4.6 Offline 插件(离线缓存)
+
+- 离线时事件存入 IndexedDB
+- 网络恢复后自动补发
+- 最多缓存 1000 条,超出丢弃最旧的
+- 补发时添加 `offline: true` 标记
+
+### 4.7 Breadcrumbs 插件(面包屑)
+
+记录用户操作轨迹,错误发生时一并上报:
+
+```typescript
+interface Breadcrumb {
+ type: 'navigation' | 'click' | 'http' | 'console' | 'user';
+ message: string;
+ timestamp: number;
+ category?: string;
+ data?: Record;
+ level: 'info' | 'warning' | 'error';
+}
+```
+
+默认保留最近 20 条面包屑。
+
+---
+
+## 五、SDK API
+
+### 5.1 初始化
+
+```javascript
+import LightSDK from 'light-sentry';
+
+LightSDK.init({
+ dsn: 'https://proj_abc123@log.example.com/1001',
+ release: '1.0.0',
+ environment: 'production',
+ sampleRate: 1,
+
+ // 插件配置
+ plugins: ['error', 'performance', 'network'],
+
+ // 性能插件配置
+ performance: {
+ sampleRate: 0.1,
+ captureLongTasks: true,
+ },
+
+ // 网络插件配置
+ network: {
+ ignoreUrls: ['/health', '/metrics'],
+ captureBody: false,
+ },
+
+ // 发送前回调
+ beforeSend(event) {
+ // 可修改或过滤事件
+ if (event.message.includes('ignore')) return null;
+ return event;
+ },
+});
+```
+
+### 5.2 手动 API
+
+```javascript
+// 捕获错误
+LightSDK.captureException(error);
+LightSDK.captureMessage('Something happened', 'warning');
+
+// 设置用户
+LightSDK.setUser({ id: '123', username: 'test' });
+
+// 设置标签
+LightSDK.setTag('page', 'home');
+LightSDK.setTags({ platform: 'ios', version: '2.0' });
+
+// 添加上下文
+LightSDK.setExtra('order_id', '12345');
+
+// 添加面包屑
+LightSDK.addBreadcrumb({
+ type: 'user',
+ message: 'User clicked button',
+ category: 'action',
+});
+
+// 手动上报
+LightSDK.captureEvent({
+ type: 'custom',
+ message: 'custom event',
+ level: 'info',
+ extra: { foo: 'bar' },
+});
+
+// 强制刷新队列
+LightSDK.flush();
+
+// 禁用 / 启用
+LightSDK.disable();
+LightSDK.enable();
+```
+
+### 5.3 框架集成
+
+#### Vue 2
+
+```javascript
+import LightSDK from 'light-sentry';
+import { VueIntegration } from 'light-sentry/vue';
+
+LightSDK.init({
+ dsn: '...',
+ plugins: [new VueIntegration(Vue)],
+});
+```
+
+#### Vue 3
+
+```javascript
+app.use(LightSDK.vuePlugin, { dsn: '...' });
+```
+
+#### React
+
+```javascript
+// 错误边界
+import { ErrorBoundary } from 'light-sentry/react';
+
+出错了
}>
+
+
+```
+
+---
+
+## 六、性能优化
+
+### 6.1 不影响页面性能的设计
+
+1. **全部异步**:SDK 初始化、事件处理、上报全异步
+2. **requestIdleCallback**:非紧急操作放在空闲时间
+3. **防抖节流**:scroll、resize 等高频事件防抖
+4. **不修改原型**:用包装方式(fetch/xhr),不污染原型
+5. **微任务批量**:同一批事件合并处理,减少事件循环开销
+6. **Web Worker**:可选,重计算(指纹、聚合)放到 Worker
+
+### 6.2 数据体积优化
+
+这是轻量 SDK 与 Sentry 的核心区别之一——通过策略大幅减少上报数据量。
+
+#### 6.2.1 错误堆栈精简
+
+| 项目 | Sentry 默认 | 轻量方案 | 节省 |
+|------|------------|----------|------|
+| 堆栈深度 | 50 帧 | **3-5 帧**(in_app) | ~80% |
+| node_modules 帧 | 保留 | **过滤掉** | ~50% |
+| 列号 | 保留 | 可选,默认开 | - |
+| 完整文件名 | 完整 URL | **只留相对路径** | ~30% |
+
+**为什么 3-5 帧就够了?**
+- 90% 的错误,看前 3 帧就能定位问题
+- 真正需要深度堆栈的错误很少,遇到了再开完整模式
+- 业务代码的调用链通常不深
+
+**配置方式**:
+```javascript
+LightSDK.init({
+ error: {
+ maxStackFrames: 5, // 最多 5 帧
+ captureNodeModules: false, // 不上报 node_modules 里的帧
+ captureColumn: true, // 保留列号
+ relativePathOnly: true, // 只保留相对路径,去掉域名
+ }
+});
+```
+
+**堆栈对比**:
+
+Sentry(2KB+):
+```
+at XMLHttpRequest.onloadend (http://localhost:5173/node_modules/.vite/deps/axios.js?v=6b332a51:1845:7)
+at settle (http://localhost:5173/node_modules/.vite/deps/axios.js?v=6b332a51:1441:12)
+at http://localhost:5173/node_modules/.vite/deps/axios.js?v=6b332a51:1245:12
+at wrapFn (http://localhost:5173/node_modules/.vite/deps/axios.js?v=6b332a51:2312:45)
+at fetchUser (http://localhost:5173/src/api/user.js:23:15)
+at UserList.vue:45:8
+...(还有 40 多帧)
+```
+
+轻量版(~200B):
+```
+at fetchUser (src/api/user.js:23)
+at UserList.vue:45
+```
+
+#### 6.2.2 环境信息结构化(browser / os / device)
+
+**问题**:每条事件都带完整的环境信息,数据冗余严重。
+
+```
+// 每条都带:~200 bytes
+"browser": { "name": "Chrome", "version": "120.0.0.0" },
+"os": { "name": "Mac OS X", "version": "10.15.7" },
+"device": { "family": "MacBook Pro", "model": "MacBook Pro" }
+```
+
+**优化策略:三级方案**
+
+##### 方案 1:字典编码(SDK 端,最简单)
+
+把常用的浏览器/OS 映射成短编码,减少传输体积。
+
+```javascript
+// 内置字典
+const BROWSER_MAP = {
+ 'Chrome': 'c',
+ 'Safari': 's',
+ 'Firefox': 'f',
+ 'Edge': 'e',
+ 'Mobile Chrome': 'cm',
+ 'Mobile Safari': 'sm',
+ // ...
+};
+
+const OS_MAP = {
+ 'Windows': 'w',
+ 'Mac OS X': 'm',
+ 'Linux': 'l',
+ 'iOS': 'i',
+ 'Android': 'a',
+ // ...
+};
+
+// 上报时用编码
+{
+ "env": {
+ "b": "c", // browser = Chrome
+ "bv": "120", // 只留主版本号
+ "os": "m", // os = Mac OS X
+ "osv": "15", // 只留大版本
+ "dv": "mbp" // device = MacBook Pro
+ }
+}
+```
+
+**效果**:环境信息从 ~200B → ~50B,减少 75%。
+
+##### 方案 2:会话级共享(进一步优化)
+
+同一个用户同一次会话里,环境信息是不变的,不需要每条都带。
+
+```
+┌─────────────────────────────────────────┐
+│ 第一次上报:带完整环境信息 + session_id │
+│ 后续上报:只带 session_id │
+└─────────────────────────────────────────┘
+```
+
+服务端根据 `session_id` 关联环境信息。
+
+**效果**:每条事件节省 50-100B,百万级事件能省几十 GB。
+
+##### 方案 3:服务端 UA 解析(最极致)
+
+SDK 完全不解析 UA,只上报原始 User-Agent 字符串,服务端统一解析后写入维度表。
+
+```
+SDK 上报:只带 1 个 ua 字段(原始 UA 字符串)
+服务端:解析后存入维度表,日志里只存维度 ID
+```
+
+| 维度 | 说明 |
+|------|------|
+| 优点 | SDK 更小(不用带 UA 解析库),解析逻辑集中维护 |
+| 缺点 | 服务端多了一层解析开销 |
+
+**推荐组合**:SDK 做字典编码 + 只保留主版本号 → 简单、够用、SDK 体积增加很少。
+
+#### 6.2.3 上下文信息分层策略
+
+不是所有事件都需要完整上下文,按级别区分:
+
+| 事件级别 | 堆栈 | 环境信息 | breadcrumbs | request 信息 | 用户信息 |
+|----------|------|----------|-------------|-------------|----------|
+| fatal | 5 帧 | 完整 | 20 条 | 完整 | 完整 |
+| error | 5 帧 | 精简 | 10 条 | URL+状态码 | id 即可 |
+| warning | 3 帧 | 精简 | 5 条 | URL 即可 | id 即可 |
+| info | 无 | 编码 | 无 | 无 | 无 |
+| debug | 无 | 无 | 无 | 无 | 无 |
+
+**配置方式**:
+```javascript
+LightSDK.init({
+ contextLevel: {
+ fatal: 'full',
+ error: 'standard',
+ warning: 'minimal',
+ info: 'none',
+ }
+});
+```
+
+#### 6.2.4 批量上报共享元数据
+
+同批次的事件,共享相同的元数据(release、environment、user 等),不用每条都重复带。
+
+**Sentry 方式(每条都带)**:
+```
+event1: { release: "1.0.0", environment: "prod", user: {...}, ... }
+event2: { release: "1.0.0", environment: "prod", user: {...}, ... }
+event3: { release: "1.0.0", environment: "prod", user: {...}, ... }
+```
+
+**轻量方式(共享头)**:
+```json
+{
+ "meta": {
+ "release": "1.0.0",
+ "environment": "prod",
+ "user": { "id": "123" },
+ "env": { "b": "c", "os": "m" }
+ },
+ "events": [
+ { "type": "error", "message": "...", "stack": [...] },
+ { "type": "performance", "metric": "lcp", "value": 2500 },
+ { "type": "error", "message": "...", "stack": [...] }
+ ]
+}
+```
+
+**效果**:批量 10 条的话,元数据部分减少约 90%。
+
+#### 6.2.5 业务错误智能过滤
+
+401、403、表单校验失败这类「预期内的业务错误」,不需要完整上报:
+
+```javascript
+network: {
+ // 忽略的 HTTP 状态码(业务预期错误)
+ ignoreStatusCodes: [401, 403, 422, 404],
+
+ // 错误请求只上报精简信息(URL + 状态码 + 耗时)
+ errorRequestDetail: 'minimal',
+
+ // 成功请求完全不上报
+ captureSuccess: false,
+
+ // 只上报 5xx 和网络错误为「错误事件」
+ // 4xx 只计数,不上报详情
+}
+```
+
+**对比**:
+| 方式 | 一个 401 错误上报体积 |
+|------|---------------------|
+| Sentry | ~2-3 KB(完整堆栈+上下文) |
+| 轻量版(标准) | ~200B(堆栈裁剪+精简) |
+| 轻量版(业务错误过滤) | ~50B(只计数)甚至 0B(忽略) |
+
+#### 6.2.6 请求头 / 响应头精简
+
+请求头和响应头是「隐藏的体积大户」——Sentry 默认会带一堆 headers,一对请求响应头就能有 1-2KB。
+
+**问题**:
+```json
+// Sentry 默认带上的 headers(节选,完整有 20+ 个)
+{
+ "request": {
+ "headers": {
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
+ "Content-Type": "application/json",
+ "Accept": "application/json, text/plain, */*",
+ "Accept-Language": "zh-CN,zh;q=0.9",
+ "Accept-Encoding": "gzip, deflate, br",
+ "Cache-Control": "no-cache",
+ "Pragma": "no-cache",
+ "Referer": "https://example.com/page",
+ "Origin": "https://example.com",
+ "Connection": "keep-alive",
+ "X-Request-Id": "abc123...",
+ "Authorization": "Bearer xxx..." // ⚠️ 敏感信息!
+ }
+ }
+}
+```
+
+**核心策略:白名单 + 编码 + 不上报 body**
+
+##### 策略 1:白名单机制(默认只带必要的)
+
+不是所有 header 都有价值,默认只上报对排查问题有用的几个:
+
+```javascript
+network: {
+ // 请求头白名单(默认)
+ captureRequestHeaders: [
+ 'content-type',
+ 'user-agent', // 或者干脆不带,环境信息里已有
+ 'x-request-id',
+ ],
+
+ // 响应头白名单(默认)
+ captureResponseHeaders: [
+ 'content-type',
+ 'content-length',
+ 'x-request-id',
+ 'server',
+ ],
+
+ // ❌ 绝对禁止上报的 header(黑名单,防止敏感信息泄露)
+ forbiddenHeaders: [
+ 'authorization',
+ 'cookie',
+ 'set-cookie',
+ 'token',
+ 'api-key',
+ 'x-api-key',
+ 'x-auth-token',
+ ],
+}
+```
+
+**效果**:从 20+ 个 header → 3-5 个,体积减少 80%+。
+
+##### 策略 2:Header 名称短编码
+
+把常见的 header 名称映射成短编码,进一步减少体积。
+
+```javascript
+const HEADER_NAME_MAP = {
+ 'content-type': 'ct',
+ 'user-agent': 'ua',
+ 'x-request-id': 'rid',
+ 'content-length': 'cl',
+ 'server': 'sv',
+ 'accept': 'ac',
+ 'referer': 'ref',
+ 'origin': 'org',
+ // ...
+};
+```
+
+**编码前后对比**:
+```json
+// 编码前(~150B)
+{ "headers": { "content-type": "application/json", "x-request-id": "abc123" } }
+
+// 编码后(~70B)
+{ "h": { "ct": "application/json", "rid": "abc123" } }
+```
+
+##### 策略 3:Header 值字典化
+
+对于值相对固定的 header,也可以做字典映射:
+
+```javascript
+// Content-Type 的常见值
+const CONTENT_TYPE_MAP = {
+ 'application/json': '1',
+ 'text/html': '2',
+ 'text/plain': '3',
+ 'application/x-www-form-urlencoded': '4',
+ 'multipart/form-data': '5',
+ 'image/png': '6',
+ // ...
+};
+```
+
+**效果**:每个 header 值从十几个字符 → 1-2 个字符。
+
+##### 策略 4:body 默认不上报
+
+请求体和响应体是最大的体积来源,而且:
+- 可能包含敏感数据(密码、token、用户信息)
+- 体积大(几 KB 到几 MB 都有可能)
+- 大部分场景下,排查问题不需要看 body
+
+```javascript
+network: {
+ // ❌ 默认不上报请求/响应体
+ captureRequestBody: false,
+ captureResponseBody: false,
+
+ // (可选)只对特定接口上报 body,且做脱敏
+ captureBodyForUrls: [
+ '/api/debug/*',
+ ],
+
+ // body 大小限制(即使开启也截断)
+ maxBodySize: 2048, // 最多 2KB
+}
+```
+
+#### 6.2.7 进一步精简:协议层与传输层
+
+前面的优化都是「内容层面」的精简,协议和传输层面还有很大空间。
+
+##### 优化 1:全量字段短编码
+
+不只是 header,**所有字段名**都可以用短编码,减少 JSON key 的重复开销。
+
+```javascript
+// 字段名映射表
+const FIELD_MAP = {
+ // 通用字段
+ 'type': 't',
+ 'level': 'l',
+ 'message': 'm',
+ 'timestamp': 'ts',
+ 'release': 'r',
+ 'environment': 'e',
+ 'platform': 'p',
+
+ // 错误相关
+ 'exception': 'ex',
+ 'stacktrace': 'st',
+ 'frames': 'fr',
+ 'filename': 'fn',
+ 'function': 'fc',
+ 'lineno': 'ln',
+ 'colno': 'cn',
+ 'in_app': 'ia',
+
+ // 用户
+ 'user': 'u',
+ 'user.id': 'uid',
+
+ // 标签/扩展
+ 'tags': 'tg',
+ 'extra': 'xt',
+
+ // 请求
+ 'request': 'req',
+ 'url': 'u',
+ 'headers': 'h',
+
+ // 性能
+ 'metric': 'mt',
+ 'value': 'v',
+ 'unit': 'un',
+ 'rating': 'rt',
+};
+```
+
+**编码前后对比**:
+```json
+// 编码前(~120B)
+{
+ "type": "error",
+ "level": "error",
+ "message": "Something broke",
+ "timestamp": 1704067200000,
+ "release": "1.0.0"
+}
+
+// 编码后(~65B)
+{
+ "t": "e",
+ "l": "e",
+ "m": "Something broke",
+ "ts": 1704067200000,
+ "r": "1.0.0"
+}
+```
+
+**效果**:字段名部分减少 50-60%。
+
+##### 优化 2:时间戳相对化
+
+同一批事件的时间戳都很接近,用「相对于批次起始时间」的差值表示,数字更小、字符更少。
+
+```json
+// 传统方式(每条都是完整时间戳)
+{
+ "events": [
+ { "ts": 1704067200123, ... },
+ { "ts": 1704067200456, ... },
+ { "ts": 1704067200789, ... }
+ ]
+}
+
+// 相对时间戳(批次头存基准时间,每条存差值)
+{
+ "base_ts": 1704067200000,
+ "events": [
+ { "ts": 123, ... },
+ { "ts": 456, ... },
+ { "ts": 789, ... }
+ ]
+}
+```
+
+**效果**:每条事件时间戳从 13 位数字 → 2-4 位,减少 60-80%。
+
+##### 优化 3:批量内字符串去重
+
+同一批事件里,很多字符串是重复的(同一页面、同一浏览器、同一版本),用「字符串表 + 索引」的方式。
+
+```json
+// 传统方式(每条都带完整字符串)
+{
+ "events": [
+ { "page": "/home", "release": "1.0.0", "browser": "Chrome" },
+ { "page": "/home", "release": "1.0.0", "browser": "Chrome" },
+ { "page": "/about", "release": "1.0.0", "browser": "Safari" }
+ ]
+}
+
+// 字符串表方式(共享字符串,只用索引引用)
+{
+ "strings": ["/home", "1.0.0", "Chrome", "/about", "Safari"],
+ "events": [
+ { "page": 0, "release": 1, "browser": 2 },
+ { "page": 0, "release": 1, "browser": 2 },
+ { "page": 3, "release": 1, "browser": 4 }
+ ]
+}
+```
+
+**效果**:批量越大、重复越多,节省越明显。10 条以上批量时,减少 30-50%。
+
+##### 优化 4:二进制协议替代 JSON
+
+JSON 是文本格式,冗余度很高。用二进制协议可以进一步压缩:
+
+| 协议 | 体积 | 解析速度 | 可读性 |
+|------|------|----------|--------|
+| JSON | 100%(基准) | 慢 | 好 |
+| MessagePack | 约 60-70% | 快 | 差(需工具解码) |
+| Protobuf | 约 40-50% | 最快 | 最差 |
+| 自定义二进制 | 约 30-40% | 快 | 最差 |
+
+**推荐策略**:
+- 默认用 JSON(简单、调试方便)
+- 高性能模式下可选 MessagePack
+- 一般项目 JSON 足够,二进制增加复杂度收益不一定高
+
+##### 优化 5:智能采样(不是全量 10%,而是动态调整)
+
+不是所有事件都值得同样的采样率,按「价值」动态调整:
+
+```javascript
+sampling: {
+ // 错误事件:100% 采样
+ error: 1.0,
+
+ // 性能指标:默认 10%,但 LCP > 4s 的 100% 采样(慢的全要)
+ performance: {
+ default: 0.1,
+ poor: 1.0, // 评级为 poor 的全采
+ needsImprovement: 0.5,
+ good: 0.05,
+ },
+
+ // 网络请求:错误 100%,成功 1%
+ network: {
+ error: 1.0,
+ success: 0.01,
+ // 慢请求也全采
+ slowThreshold: 3000,
+ slow: 1.0,
+ },
+
+ // 同一个用户全采或全不采(保证会话完整)
+ userBased: true,
+}
+```
+
+**为什么智能采样更好?**
+- 「好的」性能数据 99% 都是正常的,采多了没用
+- 「差的」性能数据才是我们真正想看的,要全采
+- 同样,成功的请求采 1% 就够统计了,失败的才需要详细排查
+
+**效果**:总体数据量再减 50-70%,同时保证「有价值的数据」不丢。
+
+##### 优化 6:相同错误聚合上报(最狠的一招)
+
+同一个错误 1 分钟内出现 100 次,没必要上报 100 次完整堆栈。
+
+```
+第 1 次:上报完整信息(堆栈 + 上下文)
+第 2~N 次:只上报计数 + 时间戳(几十字节)
+```
+
+```json
+{
+ "events": [
+ // 完整错误(首次出现)
+ {
+ "t": "e",
+ "m": "Cannot read property 'foo' of undefined",
+ "st": [...],
+ "full": true
+ },
+ // 聚合计数(后续同指纹错误)
+ {
+ "fp": "a1b2c3d4", // 错误指纹
+ "cnt": 15, // 15 次
+ "ts_start": 123, // 起始时间偏移
+ "ts_end": 456 // 结束结束
+ }
+ ]
+}
+```
+
+**效果**:对于高频错误,减少 90%+ 的上报量。
+
+#### 6.2.8 传输层优化
+
+数据体积之外,传输效率也很重要:
+
+##### 优化 1:gzip 压缩
+
+所有上报请求开启 gzip 压缩:
+- JSON 数据压缩率通常有 60-80%
+- 体积从 300B → 60-120B
+- Nginx 层自动解压或直接透传
+
+##### 优化 2:HTTP 连接复用
+
+- 开启 `keep-alive`,复用 TCP 连接
+- 减少 TCP 握手开销
+- 用同一个连接发多批数据
+
+##### 优化 3:HTTP/2
+
+- 多路复用,多个请求并行
+- 头部压缩(HPACK)
+- 服务端支持的话自动启用
+
+##### 优化 4:上报时机优化
+
+- 尽量在网络空闲时上报(`navigator.onLine` + `requestIdleCallback`)
+- 页面加载高峰期不上报,避免影响页面性能
+- 网络从离线变在线时,补发离线缓存的数据
+
+#### 6.2.9 服务端二次精简
+
+SDK 已经精简过了,服务端入库前还可以再来一刀:
+
+1. **字段过滤**:再次检查,去掉不需要的字段
+2. **堆栈再裁剪**:生产环境再裁掉几帧
+3. **敏感信息二次脱敏**:防止 SDK 端漏网之鱼
+4. **采样降频**:超过配额的项目,服务端再降采样
+5. **冷热分层**:热数据(7天)存完整信息,冷数据(30天+)只存聚合统计
+
+---
+
+#### 6.2.10 体积优化总览
+
+| 优化项 | Sentry 基线 | 优化后 | 减少比例 |
+|--------|------------|--------|---------|
+| 堆栈深度 | 50 帧 | 5 帧 | 80% |
+| node_modules 帧 | 保留 | 过滤 | 50% |
+| 环境信息 | 完整明细 | 字典编码 + 维度表 | 75-95% |
+| 请求头/响应头 | 20+ 个全量 | 白名单 + 短编码 | 80-90% |
+| 请求/响应 body | 默认带上 | 默认不上报 | 100% |
+| 字段名 | 完整名称 | 短编码(t/l/m/ts...) | 50-60% |
+| 时间戳 | 完整 13 位 | 相对时间戳 | 60-80% |
+| 批量元数据 | 每条都带 | 共享头 + 字符串表 | 70-90% |
+| 业务错误 | 完整上报 | 只计数/忽略 | 80-100% |
+| 相同错误 | 每条都完整上报 | 第1条完整 + 其余计数 | 90%+ |
+| 采样策略 | 固定采样率 | 智能采样(按价值) | 50-70% |
+| 传输压缩 | 无 | gzip 压缩 | 60-80% |
+| **综合(普通错误,gzip前)** | **~3KB/条** | **~100-200B/条** | **93-97%** |
+| **综合(普通错误,gzip后)** | **~1KB/条** | **~20-50B/条** | **95-98%** |
+
+---
+
+### 6.3 错误安全
+
+- SDK 自身 try-catch 包裹,错误不抛出
+- 无限循环检测:相同错误 1 秒内 > 10 次自动暂停
+- 递归保护:防止 SDK 错误触发新的 SDK 错误
+
+---
+
+## 七、与 Sentry SDK 的兼容性
+
+### 7.1 兼容的部分
+
+- DSN 格式完全一致
+- 事件数据结构兼容(exception、stacktrace、user、tags 等)
+- envelope 上报格式兼容
+- API 风格类似(captureException、captureMessage、setUser 等)
+
+### 7.2 不兼容的部分
+
+- 没有 Scope 概念(简化设计)
+- 没有 Hub 概念(单实例设计)
+- 没有 Integration 概念(用 Plugin 替代)
+- 不支持完整的分布式追踪(轻量版)
+- 不支持 Session/Replay(可插件扩展)
+
+### 7.3 迁移指南
+
+从 Sentry 迁移到 Light-Sentry:
+
+```diff
+- import * as Sentry from '@sentry/browser';
++ import LightSDK from 'light-sentry';
+
+- Sentry.init({ dsn: '...', ... });
++ LightSDK.init({ dsn: '...', ... });
+
+ // API 基本一致
+- Sentry.captureException(err);
++ LightSDK.captureException(err);
+
+- Sentry.setUser({ id: '1' });
++ LightSDK.setUser({ id: '1' });
+```
diff --git a/docs/02-api-server.md b/docs/02-api-server.md
new file mode 100644
index 0000000..2a16acc
--- /dev/null
+++ b/docs/02-api-server.md
@@ -0,0 +1,492 @@
+# 服务端接入层设计文档
+
+## 一、设计原则
+
+### 1.1 核心原则
+
+- **高并发**:单节点支撑 1000+ QPS 事件上报
+- **低延迟**:接口响应 < 50ms(先入队再处理)
+- **高可用**:进程崩溃不丢数据(本地持久化队列)
+- **可扩展**:支持横向扩展,无状态设计
+- **兼容性**:完全兼容 Sentry 协议,SDK 可无缝切换
+
+### 1.2 性能指标
+
+| 指标 | 目标值 | 说明 |
+|------|--------|------|
+| 单节点 QPS | 1000+ | 4C8G 服务器 |
+| P99 响应时间 | < 50ms | 99% 请求在 50ms 内返回 |
+| 数据丢失率 | < 0.01% | 异常情况下的数据丢失率 |
+| 单事件处理耗时 | < 1ms | 从入队到写入存储 |
+
+---
+
+## 二、API 接口设计
+
+### 2.1 接口列表
+
+| 端点 | 方法 | 功能 | 优先级 |
+|------|------|------|--------|
+| `/api/{projectId}/envelope/` | POST | Envelope 批量上报 | 最高 |
+| `/api/{projectId}/store/` | POST | 单事件上报 | 高 |
+| `/api/{projectId}/nel/` | POST | NEL 网络错误上报 | 中 |
+| `/api/{projectId}/health` | GET | 健康检查 | - |
+| `/api/projects/` | GET/POST | 项目管理 | - |
+| `/api/projects/{id}` | GET/PUT/DELETE | 项目 CRUD | - |
+
+### 2.2 Envelope 接口(主要上报接口)
+
+#### URL 格式
+
+```
+POST /api/{projectId}/envelope/?sentry_key={publicKey}&sentry_version=7
+```
+
+#### 请求头
+
+| Header | 说明 | 必填 |
+|--------|------|------|
+| `Content-Type` | `application/x-sentry-envelope` 或 `application/json` | 是 |
+| `X-Sentry-Auth` | Sentry 认证头(备用方式) | 否 |
+
+#### 认证方式(二选一)
+
+**方式 1:URL 参数(推荐,兼容性好)**
+```
+?sentry_key={publicKey}&sentry_version=7
+```
+
+**方式 2:Header 方式**
+```
+X-Sentry-Auth: Sentry sentry_version=7, sentry_key={publicKey}
+```
+
+#### Envelope 格式解析
+
+标准 Envelope 格式:
+```
+# 第 1 行:envelope header(JSON)
+{"event_id":"abc123","sent_at":"2024-01-01T00:00:00Z"}
+# 第 2 行:item header(JSON)
+{"type":"event","length":123}
+# 第 3 行:item payload(JSON,长度 = length)
+{"level":"error","message":"test",...}
+# 第 4 行:下一个 item header
+{"type":"transaction","length":456}
+# ...
+```
+
+支持的 item 类型:
+
+| type | 说明 | 处理方式 |
+|------|------|----------|
+| `event` | 错误事件 | 完整处理 + 写入 Loki + 聚合 |
+| `transaction` | 性能事务 | 提取关键指标 + 聚合 |
+| `session` | 会话 | 计数 + 聚合 |
+| `attachment` | 附件 | 忽略(轻量版不支持) |
+| `profile` | 性能剖析 | 忽略 |
+| `statsd` | 客户端统计 | 忽略 |
+| `user_report` | 用户反馈 | 存储 + 计数 |
+
+#### 简化格式(自研 SDK 专用)
+
+为了减少解析开销,自研 SDK 可使用简化格式:
+
+```json
+{
+ "events": [
+ {
+ "type": "error",
+ "data": { ... }
+ },
+ {
+ "type": "performance",
+ "data": { ... }
+ }
+ ]
+}
+```
+
+Content-Type: `application/json`
+
+### 2.3 Store 接口(兼容旧版 SDK)
+
+```
+POST /api/{projectId}/store/?sentry_key={publicKey}
+Content-Type: application/json
+
+{
+ "event_id": "abc123",
+ "level": "error",
+ "message": "...",
+ "exception": { ... }
+}
+```
+
+### 2.4 NEL 接口(Network Error Logging)
+
+```
+POST /api/{projectId}/nel/
+Content-Type: application/reports+json
+
+[
+ {
+ "age": 123,
+ "type": "network-error",
+ "url": "https://example.com/",
+ "body": {
+ "sampling_fraction": 1.0,
+ "server_ip": "1.2.3.4",
+ "protocol": "h2",
+ "method": "GET",
+ "status_code": 0,
+ "elapsed_time": 123,
+ "type": "dns.failed"
+ }
+ }
+]
+```
+
+---
+
+## 三、处理流程
+
+### 3.1 整体流程
+
+```
+HTTP 请求到达
+ ↓
+Nginx (限流 + CORS + 日志)
+ ↓
+1. 认证中间件
+ ├─ 解析 projectId (URL)
+ ├─ 解析 publicKey (query / header)
+ └─ 校验项目是否存在
+ ↓
+2. 请求体解析
+ ├─ 根据 Content-Type 选择解析器
+ ├─ envelope 格式 → 逐行解析
+ └─ json 格式 → JSON.parse
+ ↓
+3. 数据清洗
+ ├─ 字段校验
+ ├─ 敏感数据脱敏
+ ├─ 计算错误指纹
+ └─ 补充默认字段
+ ↓
+4. 写入内存队列(立即返回响应)
+ ↓
+5. 异步批量消费
+ ├─ 写入 Loki(原始日志)
+ ├─ 更新 MySQL 聚合表
+ └─ 触发告警检测
+```
+
+### 3.2 认证中间件
+
+```javascript
+async function sentryAuth(req, res, next) {
+ // 1. 从 URL 中提取 projectId
+ const projectId = req.params.projectId;
+
+ // 2. 从 URL 参数或 Header 获取 publicKey
+ const publicKey = req.query.sentry_key ||
+ parseSentryAuthHeader(req.headers['x-sentry-auth']);
+
+ // 3. 校验项目是否存在
+ const project = projectStore.findByProjectId(projectId);
+ if (!project) {
+ return res.status(404).json({ detail: 'Project not found' });
+ }
+
+ // 4. 校验 publicKey
+ if (project.publicKey !== publicKey) {
+ return res.status(401).json({ detail: 'Invalid public key' });
+ }
+
+ // 5. 挂载到请求对象
+ req.project = project;
+ next();
+}
+```
+
+### 3.3 中间件顺序(重要)
+
+```
+express.raw() ← Sentry 路由先挂载,避免 express.json() 干扰
+ ↓
+sentryAuth ← DSN 认证
+ ↓
+envelopeParser ← 解析 envelope 格式
+ ↓
+rateLimit ← 限流
+ ↓
+handler ← 业务处理
+
+express.json() ← 其他 API 路由
+ ↓
+其他业务中间件
+```
+
+**关键设计**:Sentry 上报接口必须在 `express.json()` 之前挂载,否则 raw body 会被解析成 JSON,导致 envelope 格式解析失败。
+
+---
+
+## 四、内存队列设计
+
+### 4.1 队列结构
+
+```
+┌─────────────────────────────────────────┐
+│ 内存队列 (Array) │
+│ [ evt1, evt2, evt3, ..., evtN ] │
+│ ↑ ↑ │
+│ head(tail) maxSize │
+└─────────────────────────────────────────┘
+ │
+ ▼
+ ┌─────────────────────┐
+ │ 批量消费定时器 │
+ │ 每 1s 或满 100 条 │
+ └─────────┬───────────┘
+ │
+ ▼
+ ┌─────────────────────┐
+ │ 批量写入 Loki │
+ │ 批量更新 MySQL │
+ │ 告警检测 │
+ └─────────────────────┘
+```
+
+### 4.2 队列参数
+
+| 参数 | 默认值 | 说明 |
+|------|--------|------|
+| `maxQueueSize` | 10000 | 队列最大长度 |
+| `flushInterval` | 1000ms | 定时刷间隔 |
+| `batchSize` | 100 | 每批处理数量 |
+| `persistInterval` | 5000ms | 持久化间隔(防止丢数据) |
+
+### 4.3 队列满时的策略
+
+1. **新事件丢弃最旧的**:FIFO,保证新事件优先
+2. **采样降级**:队列 > 80% 时,非错误事件自动降采样
+3. **快速失败**:队列 > 95% 时,直接返回 429
+4. **本地持久化**:优雅关闭时刷入本地文件
+
+### 4.4 本地持久化(可选)
+
+防止进程崩溃丢数据:
+
+- 使用 LevelDB / SQLite 作为持久化队列
+- 入队时先写磁盘,再读内存
+- 启动时从磁盘恢复未处理的事件
+- 性能影响:QPS 从 1000+ 降到 ~500
+
+---
+
+## 五、数据清洗与脱敏
+
+### 5.1 字段校验
+
+| 字段 | 校验规则 | 不通过处理 |
+|------|----------|-----------|
+| `event_id` | 32 位 hex,可选 | 自动生成 |
+| `timestamp` | ISO 时间戳,可选 | 用服务器时间 |
+| `message` | 长度 < 8KB | 截断 |
+| `exception.stacktrace` | 深度 < 50 帧 | 截断 |
+| `breadcrumbs` | 数量 < 100 | 截断 |
+| `extra` | 大小 < 16KB | 截断 |
+| 总大小 | < 256KB | 拒绝 |
+
+### 5.2 敏感数据脱敏
+
+自动脱敏的字段:
+
+| 字段名(不区分大小写) | 脱敏方式 |
+|----------------------|----------|
+| `password`, `passwd`, `pwd` | `***` |
+| `token`, `access_token`, `refresh_token` | 前 4 位 + `***` |
+| `secret`, `api_key`, `apikey` | `***` |
+| `email` | `a***@b.com` |
+| `phone`, `mobile` | `138****1234` |
+| `id_card`, `idcard` | `110***********1234` |
+| `credit_card`, `card_no` | `6222**********1234` |
+| IP 地址 | 保留前两段(192.168.x.x) |
+
+### 5.3 错误指纹计算
+
+用于错误去重和聚合:
+
+```javascript
+function computeFingerprint(event) {
+ const { exception } = event;
+ if (!exception) {
+ return md5(event.message || 'unknown');
+ }
+
+ // 提取关键信息
+ const type = exception.type || 'Error';
+ const message = normalizeMessage(exception.value); // 脱敏 + 去变量
+ const frames = exception.stacktrace?.frames || [];
+
+ // 取前 3 个 in_app 栈帧
+ const keyFrames = frames
+ .filter(f => f.in_app !== false)
+ .slice(0, 3)
+ .map(f => `${f.filename}:${f.lineno}`);
+
+ return md5(`${type}:${message}:${keyFrames.join('|')}`);
+}
+```
+
+---
+
+## 六、限流设计
+
+### 6.1 限流维度
+
+| 维度 | 默认限制 | 说明 |
+|------|----------|------|
+| 每项目每秒 | 100 条 | 项目级 QPS 限制 |
+| 每项目每天 | 100000 条 | 项目级日配额 |
+| 每 IP 每秒 | 50 条 | IP 级 QPS 限制 |
+| 全局限流 | 1000 QPS | 服务器总 QPS |
+
+### 6.2 限流算法
+
+- **令牌桶算法**:QPS 限流用令牌桶
+- **滑动窗口**:日配额用滑动窗口
+- **内存计数**:单节点足够,分布式需 Redis
+
+### 6.3 超限处理
+
+| 超出比例 | 处理方式 |
+|----------|----------|
+| < 80% | 正常处理 |
+| 80% - 100% | 非错误事件降采样 50% |
+| 100% - 150% | 只保留错误事件,其余丢弃 |
+| > 150% | 全部丢弃,返回 429 |
+
+---
+
+## 七、批量消费设计
+
+### 7.1 消费流程
+
+```
+批次事件
+ ↓
+按项目分组
+ ↓
+按事件类型分组 (error / performance / network / ...)
+ ↓
+并行处理
+ ├─ error 类型
+ │ ├─ 写入 Loki(批量)
+ │ ├─ 更新错误聚合表(按指纹分组计数)
+ │ └─ 触发告警检测
+ ├─ performance 类型
+ │ ├─ 写入 Loki
+ │ └─ 更新性能指标表(P50/P95/P99)
+ └─ 其他类型
+ └─ 写入 Loki
+```
+
+### 7.2 Loki 批量写入
+
+使用 Loki 的 `/loki/api/v1/push` 接口,批量写入:
+
+```json
+{
+ "streams": [
+ {
+ "stream": {
+ "project_id": "1001",
+ "level": "error",
+ "type": "error"
+ },
+ "values": [
+ ["", ""],
+ ["", ""]
+ ]
+ }
+ ]
+}
+```
+
+### 7.3 MySQL 批量更新
+
+- 使用 `INSERT ... ON DUPLICATE KEY UPDATE`
+- 按批次聚合后一次性写入
+- 避免逐条更新,提升性能
+
+---
+
+## 八、错误码设计
+
+| HTTP 状态码 | 说明 | 场景 |
+|-------------|------|------|
+| 200 | 成功 | 正常接收 |
+| 204 | 成功无内容 | 同上,兼容不同 SDK |
+| 400 | 请求格式错误 | envelope 格式不对、body 为空 |
+| 401 | 认证失败 | publicKey 错误 |
+| 404 | 项目不存在 | projectId 无效 |
+| 413 | 请求体过大 | 超过 256KB |
+| 429 | 限流 | 超过配额 |
+| 500 | 服务器错误 | 内部异常 |
+
+---
+
+## 九、可观测性
+
+### 9.1 自身监控指标
+
+| 指标 | 说明 |
+|------|------|
+| `events_received_total` | 接收事件总数 |
+| `events_received_per_second` | 每秒接收数 |
+| `events_dropped_total` | 丢弃事件总数 |
+| `events_processed_total` | 处理成功总数 |
+| `queue_size` | 当前队列长度 |
+| `process_duration_ms` | 处理耗时(P50/P95/P99) |
+| `loki_write_errors_total` | Loki 写入错误数 |
+| `mysql_write_errors_total` | MySQL 写入错误数 |
+
+### 9.2 健康检查接口
+
+```
+GET /health
+
+{
+ "status": "ok",
+ "timestamp": "2024-01-01T00:00:00Z",
+ "uptime": 86400,
+ "queue_size": 123,
+ "events_processed": 1234567,
+ "loki": "connected",
+ "mysql": "connected"
+}
+```
+
+---
+
+## 十、横向扩展
+
+### 10.1 无状态设计
+
+- API Server 完全无状态
+- 项目配置缓存,启动时加载,定时刷新
+- 队列在内存中,扩展时直接加机器
+
+### 10.2 负载均衡
+
+- Nginx 层做负载均衡
+- 按 projectId 一致性哈希路由
+- 同一项目的事件打到同一台机器(有利于缓存和聚合)
+
+### 10.3 队列扩展
+
+- 小流量:内存队列足够
+- 中流量:加 Redis 作为分布式队列
+- 大流量:加 Kafka(不推荐,太重了)
diff --git a/docs/03-data-aggregation.md b/docs/03-data-aggregation.md
new file mode 100644
index 0000000..60e0344
--- /dev/null
+++ b/docs/03-data-aggregation.md
@@ -0,0 +1,533 @@
+# 数据聚合与存储设计文档
+
+## 一、设计原则
+
+### 1.1 存储分层策略
+
+```
+热数据(7天内) ←→ Loki + MySQL(高频查询)
+温数据(7-30天) ←→ Loki(低频查询)
+冷数据(30天+) ←→ 对象存储归档(极少查询)
+```
+
+### 1.2 存储选型
+
+| 数据类型 | 存储 | 保留时长 | 说明 |
+|----------|------|----------|------|
+| 原始错误事件 | Loki | 7-30 天 | 堆栈、上下文、面包屑 |
+| 原始性能事件 | Loki | 7 天 | 性能明细数据 |
+| 原始行为事件 | Loki | 3 天 | PV、点击等(量大) |
+| 错误聚合统计 | MySQL | 永久 | 按指纹 + 时间聚合 |
+| 性能指标统计 | MySQL | 90 天 | 按指标 + 时间聚合 |
+| 项目配置 | MySQL | 永久 | 项目、用户、告警规则 |
+| 告警记录 | MySQL | 90 天 | 告警历史 |
+
+### 1.3 设计目标
+
+- **Loki 存储成本**:100万错误事件 ≈ 5GB/月
+- **MySQL 存储成本**:聚合数据 ≈ 100MB/月/项目
+- **查询性能**:聚合查询 < 100ms,原始日志查询 < 2s
+
+---
+
+## 二、Loki 存储设计
+
+### 2.1 Label 设计
+
+Loki 的 Label 是查询索引,设计原则:**低基数、高区分度**。
+
+| Label | 说明 | 基数 | 示例 |
+|-------|------|------|------|
+| `project_id` | 项目 ID | 低(项目数) | 1001 |
+| `type` | 事件类型 | 极低 | error / performance / network / behavior |
+| `level` | 日志级别 | 极低 | fatal / error / warning / info |
+| `platform` | 平台 | 低 | javascript / node / python |
+| `environment` | 环境 | 低 | production / staging / development |
+| `fingerprint` | 错误指纹 | 中 | abc123def |
+
+**反模式(不要用做 Label)**:
+- ❌ `event_id`:基数太高,每个事件都不同
+- ❌ `message`:基数太高,且是文本
+- ❌ `url`:基数太高
+- ❌ `user_id`:基数太高
+
+这些应该放在日志内容里,用 grep 查询。
+
+### 2.2 日志格式(JSON)
+
+每条日志是一行 JSON,方便 Loki 的 `json` 解析器提取字段。
+
+#### 错误事件格式
+
+```json
+{
+ "event_id": "abc123...",
+ "type": "error",
+ "level": "error",
+ "timestamp": 1704067200000,
+ "project_id": "1001",
+ "platform": "javascript",
+ "environment": "production",
+ "release": "1.0.0",
+ "fingerprint": "a1b2c3d4e5f6",
+
+ "message": "Cannot read property 'foo' of undefined",
+ "exception": {
+ "type": "TypeError",
+ "value": "Cannot read property 'foo' of undefined",
+ "stacktrace": {
+ "frames": [
+ {
+ "filename": "https://example.com/app.js",
+ "function": "onClick",
+ "lineno": 123,
+ "colno": 45,
+ "in_app": true
+ }
+ ]
+ }
+ },
+
+ "user": {
+ "id": "123",
+ "username": "testuser"
+ },
+
+ "tags": {
+ "page": "/home",
+ "browser": "Chrome 120"
+ },
+
+ "extra": {},
+ "breadcrumbs": [],
+
+ "request": {
+ "url": "https://example.com/page",
+ "headers": {
+ "user_agent": "Mozilla/5.0..."
+ }
+ }
+}
+```
+
+#### 性能事件格式
+
+```json
+{
+ "type": "performance",
+ "level": "info",
+ "timestamp": 1704067200000,
+ "project_id": "1001",
+
+ "metric": "lcp",
+ "value": 2500,
+ "unit": "ms",
+ "rating": "good",
+
+ "tags": {
+ "page_url": "https://example.com/page",
+ "route": "/home",
+ "browser": "Chrome 120",
+ "os": "Mac OS X"
+ }
+}
+```
+
+#### 网络事件格式
+
+```json
+{
+ "type": "network",
+ "level": "info",
+ "timestamp": 1704067200000,
+ "project_id": "1001",
+
+ "sub_type": "fetch",
+ "method": "GET",
+ "url": "/api/users",
+ "status_code": 200,
+ "duration": 123,
+ "success": true,
+
+ "tags": {
+ "route": "/home"
+ }
+}
+```
+
+### 2.3 Loki 查询示例
+
+**查询某项目的错误总数(1小时内)**
+```logql
+count_over_time(
+ {project_id="1001", type="error"}[1h]
+)
+```
+
+**查询某错误指纹的出现次数**
+```logql
+count_over_time(
+ {project_id="1001", fingerprint="a1b2c3d4"}[24h]
+)
+```
+
+**查询某页面的 LCP P95**
+```logql
+quantile_over_time(
+ 0.95,
+ {project_id="1001", type="performance"}
+ | json value=value
+ | metric="lcp"
+ | unwrap value
+ [5m]
+)
+```
+
+---
+
+## 三、MySQL 聚合表设计
+
+### 3.1 错误聚合表
+
+#### `error_stats_hourly` - 错误小时统计表
+
+按错误指纹 + 项目 + 小时聚合,用于趋势图。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT PK | 主键 |
+| `project_id` | VARCHAR(64) | 项目 ID |
+| `fingerprint` | VARCHAR(64) | 错误指纹 |
+| `stat_hour` | DATETIME | 统计小时(整点) |
+| `error_type` | VARCHAR(128) | 错误类型(TypeError 等) |
+| `error_message` | VARCHAR(512) | 错误消息(截断) |
+| `count` | INT | 发生次数 |
+| `affected_users` | INT | 影响用户数(估算) |
+| `first_seen` | DATETIME | 首次出现时间 |
+| `last_seen` | DATETIME | 最后出现时间 |
+| `created_at` | DATETIME | 创建时间 |
+| `updated_at` | DATETIME | 更新时间 |
+
+**索引**:
+- `(project_id, stat_hour)` - 按项目+时间查询
+- `(project_id, fingerprint, stat_hour)` - 按指纹+时间查询
+- 唯一键:`(project_id, fingerprint, stat_hour)`
+
+#### `error_issues` - 错误 Issue 表
+
+按错误指纹聚合,用于错误列表管理。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT PK | 主键 |
+| `project_id` | VARCHAR(64) | 项目 ID |
+| `fingerprint` | VARCHAR(64) UNIQUE | 错误指纹 |
+| `error_type` | VARCHAR(128) | 错误类型 |
+| `error_message` | TEXT | 错误消息 |
+| `stack_trace` | TEXT | 堆栈摘要(前几帧) |
+| `level` | VARCHAR(32) | 级别 |
+| `platform` | VARCHAR(32) | 平台 |
+| `total_count` | BIGINT | 总次数 |
+| `today_count` | INT | 今日次数 |
+| `yesterday_count` | INT | 昨日次数 |
+| `affected_users` | INT | 影响用户数 |
+| `status` | VARCHAR(32) | 状态:active / resolved / ignored |
+| `assignee` | VARCHAR(64) | 处理人 |
+| `first_seen` | DATETIME | 首次出现 |
+| `last_seen` | DATETIME | 最后出现 |
+| `created_at` | DATETIME | 创建时间 |
+| `updated_at` | DATETIME | 更新时间 |
+
+**状态流转**:
+```
+active ──→ resolved ──→ active(再次出现时复活)
+ │
+ └──→ ignored
+```
+
+### 3.2 性能统计表
+
+#### `performance_stats_hourly` - 性能小时统计表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT PK | 主键 |
+| `project_id` | VARCHAR(64) | 项目 ID |
+| `metric` | VARCHAR(32) | 指标名:lcp / fcp / cls / fid |
+| `stat_hour` | DATETIME | 统计小时 |
+| `page_url` | VARCHAR(256) | 页面 URL(可选,空=全部) |
+| `sample_count` | INT | 样本数 |
+| `p50` | DOUBLE | 中位数 |
+| `p75` | DOUBLE | 75 分位 |
+| `p90` | DOUBLE | 90 分位 |
+| `p95` | DOUBLE | 95 分位 |
+| `p99` | DOUBLE | 99 分位 |
+| `avg` | DOUBLE | 平均值 |
+| `good_rate` | DOUBLE | Good 比例(0-1) |
+| `poor_rate` | DOUBLE | Poor 比例(0-1) |
+| `created_at` | DATETIME | 创建时间 |
+| `updated_at` | DATETIME | 更新时间 |
+
+**索引**:
+- `(project_id, metric, stat_hour)` - 主查询索引
+- 唯一键:`(project_id, metric, stat_hour, page_url)`
+
+#### `performance_stats_daily` - 性能日统计表
+
+同上,按天聚合,用于长期趋势。
+
+### 3.3 网络请求统计表
+
+#### `network_stats_hourly` - 网络小时统计表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT PK | 主键 |
+| `project_id` | VARCHAR(64) | 项目 ID |
+| `stat_hour` | DATETIME | 统计小时 |
+| `method` | VARCHAR(16) | 请求方法 |
+| `url_pattern` | VARCHAR(512) | URL 模式(归一化后) |
+| `total_count` | INT | 总请求数 |
+| `error_count` | INT | 错误数(4xx + 5xx) |
+| `error_rate` | DOUBLE | 错误率 |
+| `avg_duration` | DOUBLE | 平均耗时(ms) |
+| `p50_duration` | DOUBLE | P50 耗时 |
+| `p95_duration` | DOUBLE | P95 耗时 |
+| `created_at` | DATETIME | 创建时间 |
+
+**URL 归一化**:
+- `/api/users/123` → `/api/users/:id`
+- `/static/app.abc123.js` → `/static/app.[hash].js`
+
+### 3.4 环境维度表
+
+用于减少 Loki 中环境信息的冗余,配合 SDK 的字典编码和会话级共享使用。
+
+#### `env_dimensions` - 环境维度表
+
+每条唯一的环境组合只存 1 条,日志中通过 `env_id` 关联。
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT PK | 维度 ID |
+| `project_id` | VARCHAR(64) | 项目 ID |
+| `browser_name` | VARCHAR(32) | 浏览器名称 |
+| `browser_version` | VARCHAR(32) | 浏览器主版本 |
+| `os_name` | VARCHAR(32) | 操作系统 |
+| `os_version` | VARCHAR(32) | OS 主版本 |
+| `device_family` | VARCHAR(64) | 设备系列 |
+| `device_model` | VARCHAR(64) | 设备型号 |
+| `screen_resolution` | VARCHAR(32) | 屏幕分辨率 |
+| `language` | VARCHAR(16) | 浏览器语言 |
+| `hash` | VARCHAR(64) UNIQUE | 所有维度的哈希值,用于快速查找 |
+| `first_seen` | DATETIME | 首次出现 |
+| `last_seen` | DATETIME | 最后出现 |
+| `count` | BIGINT | 出现次数(用于热度排序) |
+
+**唯一键**:`(project_id, hash)`
+
+**为什么用维度表?**
+- Loki 的 JSON 日志里只存 `env_id`(1 个数字),不存完整的浏览器/OS/设备信息
+- 每条日志节省 100-200 字节,百万级事件节省几十 GB
+- 查询时 JOIN 维度表,或者直接用 Grafana 的变量查询
+
+**服务端处理流程**:
+```
+事件到达 → 计算环境维度的 hash →
+ ├─ 已存在 → 取 env_id,更新 last_seen + count
+ └─ 不存在 → 插入新记录,返回新 env_id
+→ 日志写入 Loki,只带 env_id
+```
+
+#### `page_dimensions` - 页面维度表
+
+同理,页面 URL 也可以做维度化:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT PK | 页面 ID |
+| `project_id` | VARCHAR(64) | 项目 ID |
+| `url_pattern` | VARCHAR(512) | 归一化后的 URL 模式 |
+| `path` | VARCHAR(512) | 路由路径 |
+| `title` | VARCHAR(256) | 页面标题 |
+| `hash` | VARCHAR(64) UNIQUE | 哈希 |
+| `count` | BIGINT | 访问次数 |
+
+#### `api_dimensions` - 接口维度表
+
+网络请求的 URL、method 等也可以维度化,日志里只存 `api_id`:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | BIGINT PK | 接口 ID |
+| `project_id` | VARCHAR(64) | 项目 ID |
+| `method` | VARCHAR(16) | 请求方法(GET/POST/...) |
+| `url_pattern` | VARCHAR(512) | 归一化后的 URL 模式 |
+| `domain` | VARCHAR(128) | 域名 |
+| `hash` | VARCHAR(64) UNIQUE | method + url_pattern 的哈希 |
+| `total_count` | BIGINT | 总请求数 |
+| `error_count` | BIGINT | 错误数 |
+| `avg_duration` | DOUBLE | 平均耗时 |
+| `last_seen` | DATETIME | 最后出现时间 |
+
+**为什么要做接口维度化?**
+- 日志里只存 `api_id`(8 字节),不用存完整 URL
+- URL 归一化后,同一路由的不同参数只会产生 1 条维度记录
+- 按接口统计错误率、耗时等指标时,直接 JOIN 维度表即可
+
+### 3.5 项目配置表
+
+#### `projects` - 项目表
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `id` | VARCHAR(64) PK | 内部 ID(proj_xxx) |
+| `project_id` | VARCHAR(32) UNIQUE | Sentry 项目 ID(纯数字) |
+| `public_key` | VARCHAR(64) | 公钥(默认=内部 ID) |
+| `name` | VARCHAR(128) | 项目名称 |
+| `platform` | VARCHAR(32) | 平台 |
+| `environment` | VARCHAR(32) | 默认环境 |
+| `description` | TEXT | 描述 |
+| `status` | VARCHAR(32) | 状态:active / disabled |
+| `rate_limit` | INT | 速率限制(事件/天) |
+| `sample_rate` | DOUBLE | 采样率(0-1) |
+| `created_at` | DATETIME | 创建时间 |
+| `updated_at` | DATETIME | 更新时间 |
+
+#### `alert_rules` - 告警规则表
+
+见告警引擎设计文档。
+
+---
+
+## 四、聚合任务设计
+
+### 4.1 聚合方式
+
+| 方式 | 实时性 | 复杂度 | 适用场景 |
+|------|--------|--------|----------|
+| **实时增量聚合** | 秒级 | 中 | 错误计数、Issue 更新 |
+| **定时批量聚合** | 分钟级 | 低 | 性能分位数、小时统计 |
+| **离线重算** | 天级 | 高 | 数据修正、历史回刷 |
+
+### 4.2 实时增量聚合(错误计数)
+
+每次事件处理时,直接更新 MySQL:
+
+```sql
+INSERT INTO error_issues
+ (project_id, fingerprint, error_type, error_message,
+ total_count, today_count, last_seen, first_seen, status)
+VALUES
+ (?, ?, ?, ?, 1, 1, NOW(), NOW(), 'active')
+ON DUPLICATE KEY UPDATE
+ total_count = total_count + 1,
+ today_count = today_count + 1,
+ last_seen = NOW(),
+ status = CASE WHEN status = 'resolved' THEN 'active' ELSE status END;
+```
+
+**优点**:实时性好,数据立刻可见
+**缺点**:高并发下 MySQL 压力大
+
+**优化**:
+- 内存中先做 1 秒级别的微批聚合,再批量写 MySQL
+- 使用 Redis 做计数缓冲,定期刷入 MySQL
+
+### 4.3 定时批量聚合(性能统计)
+
+使用 Cron 定时任务,每 5 分钟从 Loki 拉取数据聚合:
+
+```
+每 5 分钟执行:
+ 1. 从 Loki 查询过去 5 分钟的性能事件
+ 2. 按 project + metric + 页面 分组
+ 3. 计算 P50/P90/P95/avg 等指标
+ 4. 写入 performance_stats_hourly 表
+```
+
+**为什么用定时任务而不是实时?**
+- 性能指标不需要秒级实时
+- 分位数计算需要一定数据量才准确
+- 定时批量更省资源
+
+### 4.4 数据过期与归档
+
+#### Loki 数据过期
+
+通过 Loki 的 `retention` 配置自动删除:
+
+```yaml
+limits_config:
+ retention_period: 168h # 7 天
+```
+
+#### MySQL 数据过期
+
+- 小时统计表:保留 30 天
+- 日统计表:保留 1 年
+- 错误 Issue 表:永久保留(只存聚合数据,量很小)
+
+定时任务每天凌晨清理过期数据。
+
+---
+
+## 五、数据迁移与兼容
+
+### 5.1 当前状态
+
+目前项目使用 JSON 文件存储项目配置,Loki 存储原始日志,没有 MySQL 聚合层。
+
+### 5.2 演进路径
+
+**阶段 1:JSON → SQLite(单机版)**
+- 零依赖,开箱即用
+- 适合个人项目、小团队
+- 单节点足够
+
+**阶段 2:SQLite → MySQL(生产版)**
+- 支持并发
+- 性能更好
+- 适合多项目、中大型团队
+
+**阶段 3:增加 ClickHouse(大规模)**
+- 亿级数据量
+- 复杂分析查询
+- 一般不需要
+
+---
+
+## 六、Grafana 数据源配置
+
+### 6.1 Loki 数据源
+
+- URL: `http://loki:3100`
+- 开启 JSON 解析
+- 配置 Derived fields(从日志跳转到追踪)
+
+### 6.2 MySQL 数据源
+
+- 用于展示聚合数据(性能趋势、错误趋势)
+- 比 Loki 查询更稳定、更快
+
+---
+
+## 七、数据安全
+
+### 7.1 数据脱敏
+
+- 入库前脱敏:SDK 端 + 服务端双重脱敏
+- 查询时脱敏:敏感字段查询结果自动打码
+- 导出时脱敏:导出数据默认脱敏
+
+### 7.2 数据隔离
+
+- 项目间完全隔离(通过 project_id label)
+- 查询时强制带 project_id 条件
+- 管理后台有项目权限控制
+
+### 7.3 数据备份
+
+- MySQL:每日全量备份 + binlog 增量备份
+- Loki:定期快照到对象存储
+- 配置文件:Git 版本管理
diff --git a/docs/04-alert-engine.md b/docs/04-alert-engine.md
new file mode 100644
index 0000000..e2e64c6
--- /dev/null
+++ b/docs/04-alert-engine.md
@@ -0,0 +1,451 @@
+# 告警引擎设计文档
+
+## 一、设计原则
+
+### 1.1 核心原则
+
+- **快**:错误发生后 1 分钟内触发告警
+- **准**:减少误报,告警要有价值
+- **全**:支持多种告警规则和通知渠道
+- **轻**:不依赖复杂组件,轻量实现
+
+### 1.2 告警目标
+
+- 错误突增:及时发现线上故障
+- 新错误出现:第一时间感知新问题
+- 错误率超标:质量红线监控
+- 性能劣化:用户体验下降预警
+
+---
+
+## 二、告警规则类型
+
+### 2.1 规则分类
+
+| 类型 | 说明 | 实时性 | 复杂度 |
+|------|------|--------|--------|
+| **阈值告警** | 指标超过固定阈值 | 高 | 低 |
+| **突增告警** | 同比/环比增长超过阈值 | 中 | 中 |
+| **新错误告警** | 出现新的错误指纹 | 高 | 低 |
+| **错误率告警** | 错误率超过阈值 | 中 | 中 |
+| **质量分告警** | 性能/质量综合评分下降 | 低 | 高 |
+
+### 2.2 内置规则模板
+
+#### 1. 错误数量突增
+
+```yaml
+name: 错误数量突增
+type: spike
+metric: error_count
+window: 5m # 检测窗口
+compare: 1h_ago # 对比:1小时前
+threshold: 200% # 增长 200% 触发
+min_count: 10 # 最少 10 条才检测(避免噪音)
+level: warning
+```
+
+#### 2. 新错误出现
+
+```yaml
+name: 新错误出现
+type: new_error
+metric: error_fingerprint
+window: 24h # 过去 24 小时没出现过
+level: info
+```
+
+#### 3. JS 错误率过高
+
+```yaml
+name: JS 错误率过高
+type: threshold
+metric: error_rate # 错误数 / PV
+window: 5m
+threshold: 5% # 错误率超过 5%
+level: critical
+```
+
+#### 4. 页面性能劣化
+
+```yaml
+name: LCP 性能劣化
+type: threshold
+metric: lcp_p95
+window: 15m
+threshold: 4000 # P95 LCP > 4s
+level: warning
+```
+
+#### 5. 接口错误率过高
+
+```yaml
+name: 接口错误率过高
+type: threshold
+metric: api_error_rate
+window: 5m
+threshold: 10%
+level: critical
+```
+
+---
+
+## 三、告警引擎架构
+
+### 3.1 整体架构
+
+```
+┌──────────────────────────────────────────────────────┐
+│ 事件流 │
+│ 错误事件 ──→ 实时检测 ──→ 触发规则 ──→ 告警通知 │
+│ (快路径) │
+└──────────────────────────────────────────────────────┘
+
+┌──────────────────────────────────────────────────────┐
+│ 定时任务 │
+│ Loki/MySQL ──→ 指标计算 ──→ 规则匹配 ──→ 告警通知 │
+│ (慢路径) │
+└──────────────────────────────────────────────────────┘
+```
+
+### 3.2 双路径设计
+
+#### 快路径:实时检测(秒级)
+
+用于:新错误出现、错误数突增(粗粒度)
+
+```
+事件入队
+ ↓
+实时规则引擎
+ ├─ 检查是否是新指纹(内存 BloomFilter)
+ ├─ 检查 1 分钟错误数是否突增
+ └─ 触发 → 入告警队列
+ ↓
+告警收敛 + 去重
+ ↓
+通知
+```
+
+#### 慢路径:定时检测(分钟级)
+
+用于:复杂计算、分位数、错误率
+
+```
+定时任务(每 5 分钟)
+ ↓
+从 Loki/MySQL 拉取指标
+ ↓
+计算同比、环比、分位数
+ ↓
+匹配告警规则
+ ↓
+触发 → 入告警队列
+ ↓
+告警收敛 + 去重
+ ↓
+通知
+```
+
+---
+
+## 四、告警规则引擎
+
+### 4.1 规则数据结构
+
+```sql
+CREATE TABLE alert_rules (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ project_id VARCHAR(64) NOT NULL,
+ name VARCHAR(128) NOT NULL,
+ description TEXT,
+
+ -- 规则类型
+ type VARCHAR(32) NOT NULL, -- threshold / spike / new_error / error_rate
+
+ -- 规则配置(JSON)
+ config JSON NOT NULL,
+ /*
+ {
+ "metric": "error_count",
+ "window": "5m",
+ "threshold": 100,
+ "compare": "1h_ago",
+ "operator": ">"
+ }
+ */
+
+ -- 告警级别
+ level VARCHAR(32) NOT NULL, -- info / warning / critical
+
+ -- 通知渠道
+ channels JSON NOT NULL,
+ /*
+ [
+ { "type": "webhook", "url": "https://..." },
+ { "type": "email", "to": ["a@b.com"] }
+ ]
+ */
+
+ -- 收敛配置
+ group_by VARCHAR(64), -- 按指纹/项目/页面分组
+ interval INT DEFAULT 300, -- 同组告警间隔(秒)
+ max_count INT DEFAULT 10, -- 每小时最多告警次数
+
+ -- 状态
+ enabled TINYINT DEFAULT 1,
+ created_at DATETIME,
+ updated_at DATETIME,
+
+ INDEX idx_project (project_id)
+);
+```
+
+### 4.2 规则匹配流程
+
+```
+新指标数据到达
+ ↓
+加载项目的所有启用规则
+ ↓
+逐条匹配
+ ├─ 类型匹配?
+ ├─ 条件满足?
+ └─ 未被静默?
+ ↓
+触发告警
+ ↓
+告警去重 + 收敛
+ ↓
+发送通知
+```
+
+### 4.3 内置规则表达式
+
+支持简单的表达式语法:
+
+```
+# 阈值比较
+error_count > 100
+error_rate > 0.05
+lcp_p95 >= 4000
+
+# 同比环比
+error_count / error_count_1h_ago > 2
+error_count > error_count_1d_ago * 1.5
+```
+
+---
+
+## 五、告警收敛与降噪
+
+### 5.1 降噪策略
+
+| 策略 | 说明 | 效果 |
+|------|------|------|
+| **同指纹去重** | 同一错误 5 分钟内只告警 1 次 | 减少 80% 重复告警 |
+| **分组收敛** | 按项目/级别聚合,合并发送 | 减少通知数量 |
+| **频率限制** | 每项目每小时最多 N 条 | 防止告警风暴 |
+| **静默期** | 已知问题可设置静默 | 忽略已知问题 |
+| **最小阈值** | 数量太少不告警 | 避免噪音 |
+
+### 5.2 告警去重键
+
+```
+去重键 = project_id + rule_id + fingerprint + 时间窗口
+```
+
+同一去重键在 `interval` 时间内只发 1 次告警。
+
+### 5.3 告警升级
+
+- 持续时间超过 30 分钟 → 级别升级(warning → critical)
+- 影响用户数超过阈值 → 级别升级
+- 持续超过 2 小时 → 通知更多人
+
+---
+
+## 六、通知渠道
+
+### 6.1 支持的渠道
+
+| 渠道 | 说明 | 适用场景 |
+|------|------|----------|
+| **Webhook** | 通用 HTTP 回调 | 飞书、钉钉、企业微信、Slack |
+| **邮件** | SMTP 发送 | 正式通知、归档 |
+| **Server酱** | 微信推送 | 个人项目 |
+| **飞书机器人** | 专用适配 | 飞书团队 |
+| **钉钉机器人** | 专用适配 | 钉钉团队 |
+
+### 6.2 Webhook 格式
+
+```json
+{
+ "alert_id": "alert_abc123",
+ "rule_id": 123,
+ "rule_name": "错误数量突增",
+ "level": "critical",
+ "project_id": "1001",
+ "project_name": "前端项目",
+
+ "metric": "error_count",
+ "value": 250,
+ "threshold": 100,
+ "window": "5m",
+
+ "description": "5 分钟内错误数达到 250,超过阈值 100,增长 150%",
+
+ "details": {
+ "error_type": "TypeError",
+ "top_errors": [
+ { "fingerprint": "a1b2c3", "message": "...", "count": 100 },
+ { "fingerprint": "d4e5f6", "message": "...", "count": 50 }
+ ]
+ },
+
+ "link": "https://log.example.com/manage/#/errors?project=1001",
+ "timestamp": "2024-01-01T00:00:00Z"
+}
+```
+
+### 6.3 飞书 / 钉钉适配
+
+提供现成的模板,直接粘贴 Webhook URL 即可使用:
+
+- 飞书:使用富文本卡片,支持点击跳转
+- 钉钉:使用 Markdown 格式,支持 @人
+
+---
+
+## 七、告警事件存储
+
+### 7.1 告警历史表
+
+```sql
+CREATE TABLE alert_events (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ project_id VARCHAR(64) NOT NULL,
+ rule_id BIGINT NOT NULL,
+ rule_name VARCHAR(128),
+ level VARCHAR(32),
+
+ -- 告警数据
+ metric VARCHAR(64),
+ value DOUBLE,
+ threshold DOUBLE,
+ description TEXT,
+ details JSON,
+
+ -- 状态
+ status VARCHAR(32) DEFAULT 'firing', -- firing / resolved / acknowledged
+
+ -- 时间
+ started_at DATETIME,
+ resolved_at DATETIME NULL,
+ duration INT, -- 持续时间(秒)
+
+ created_at DATETIME,
+ updated_at DATETIME,
+
+ INDEX idx_project_time (project_id, started_at),
+ INDEX idx_status (status)
+);
+```
+
+### 7.2 状态流转
+
+```
+firing ──→ acknowledged ──→ resolved
+ │ ↑
+ └───────────────────────────┘
+ 自动恢复(指标降下来)
+```
+
+---
+
+## 八、实现方案
+
+### 8.1 轻量实现(v1)
+
+**技术栈**:Node.js + 内存定时器 + MySQL
+
+```
+API Server 进程内
+ ├─ 事件消费时同步检测(快路径)
+ │ ├─ 新错误检测(BloomFilter)
+ │ └─ 简单计数(1min 滑动窗口)
+ │
+ └─ 定时任务(慢路径)
+ └─ 每 5 分钟跑一次复杂规则
+```
+
+**优点**:无额外组件,部署简单
+**缺点**:单进程,无法水平扩展
+
+### 8.2 独立服务(v2)
+
+**技术栈**:独立的 Alert Worker 进程 + Redis
+
+```
+API Server → Redis Stream → Alert Worker
+ → 规则匹配
+ → 告警收敛
+ → 通知发送
+```
+
+**优点**:可独立扩展,不影响接入性能
+**缺点**:多一个组件
+
+---
+
+## 九、静默与抑制
+
+### 9.1 静默规则
+
+用户可以设置静默期,暂时忽略某些告警:
+
+```yaml
+- project_id: 1001
+ fingerprint: a1b2c3d4
+ reason: "已知问题,下个版本修复"
+ start_time: 2024-01-01 00:00:00
+ end_time: 2024-01-03 00:00:00
+ created_by: user1
+```
+
+### 9.2 抑制规则
+
+高优先级告警抑制低优先级:
+
+- critical 级别的错误告警触发后,同指纹的 warning 级告警被抑制
+- 项目级别的大故障告警触发后,该项目的其他告警被抑制
+
+---
+
+## 十、与现有系统集成
+
+### 10.1 当前状态
+
+- 有 Loki 存原始数据 → 可用于慢路径查询
+- 有 MySQL(待加聚合层)→ 可存规则和告警历史
+- 有管理后台 → 可加告警管理页面
+
+### 10.2 落地步骤
+
+**v1(最小可用)**:
+1. 新增 alert_rules 和 alert_events 表
+2. 实现新错误检测(快路径,内存 BloomFilter)
+3. 实现 Webhook 通知
+4. 管理后台加告警规则配置页面
+
+**v2(增强)**:
+1. 实现定时规则引擎(慢路径)
+2. 增加突增检测、错误率检测
+3. 支持飞书/钉钉专用模板
+4. 告警收敛和静默功能
+
+**v3(完善)**:
+1. 独立 Alert Worker 服务
+2. Redis 队列 + 分布式锁
+3. 更复杂的规则表达式
+4. 告警抑制和升级
diff --git a/docs/05-admin-dashboard.md b/docs/05-admin-dashboard.md
new file mode 100644
index 0000000..8c168c5
--- /dev/null
+++ b/docs/05-admin-dashboard.md
@@ -0,0 +1,916 @@
+# 管理后台与可视化设计文档
+
+## 一、设计原则
+
+### 1.1 核心理念
+
+**克制、高效、数据为先**
+
+- 不做花哨的动效,所有设计都服务于「快速定位问题」
+- 深色主题为主,长时间盯着不累眼
+- 信息分层明确,重要的信息一眼看到,次要的信息收起来
+- 操作路径短:3 次点击以内能到达任何核心页面
+
+### 1.2 技术选型
+
+| 层级 | 技术 | 说明 |
+|------|------|------|
+| 框架 | React 18 | 生态成熟,社区活跃 |
+| 构建 | Vite 5 | 快,开发体验好 |
+| 语言 | TypeScript | 类型安全,减少 bug |
+| UI 库 | Ant Design 5 | 后台管理组件最全,深色主题好 |
+| 路由 | React Router v6 | 官方推荐,功能完整 |
+| 状态管理 | Zustand | 轻量,比 Redux 简单太多 |
+| 数据请求 | TanStack Query (React Query) | 缓存、重发、分页都有了 |
+| 图表 | ECharts 5 | 功能强大,国内用得多 |
+| HTTP 客户端 | Axios | 拦截器、取消请求都方便 |
+| 样式 | CSS Modules + Less | 按需定制主题 |
+| 代码规范 | ESLint + Prettier | 统一代码风格 |
+
+### 1.3 为什么选 Ant Design
+
+- **组件最丰富**:表格、表单、弹窗、树形...后台需要的都有
+- **深色主题好**:AntD 5 原生支持 ConfigProvider 切换主题
+- **ProComponents**:ProTable、ProForm 等高级组件,开发效率翻倍
+- **生态成熟**:遇到问题搜一下就有答案
+- **设计语言统一**:有自己的设计规范,不用从零定
+
+### 1.3 页面清单
+
+| 页面 | 路径 | 优先级 | 说明 |
+|------|------|--------|------|
+| 项目列表 | `/manage/` | P0 | 项目管理(已有) |
+| 项目概览 | `#/overview` | P0 | 项目总览、关键指标 |
+| 错误列表 | `#/errors` | P0 | 错误 Issue 列表、详情 |
+| 错误详情 | `#/errors/:id` | P0 | 单个错误的详细分析 |
+| 性能分析 | `#/performance` | P1 | Web Vitals、性能趋势 |
+| 网络分析 | `#/network` | P2 | 接口请求统计 |
+| 行为分析 | `#/behavior` | P2 | PV、用户行为 |
+| 告警中心 | `#/alerts` | P1 | 告警规则、告警历史 |
+| 日志查询 | `#/logs` | P1 | Loki 原始日志查询 |
+| 接入指南 | `#/integration` | P0 | SDK 接入文档(已有) |
+| 项目设置 | `#/settings` | P1 | 项目配置、成员、采样率 |
+
+---
+
+## 二、设计系统
+
+### 2.1 配色系统
+
+**主色调**:蓝色系(专业、可信赖)
+
+| 用途 | 颜色 | 色值 |
+|------|------|------|
+| 品牌主色 | 亮蓝 | `#0090f9` |
+| 品牌主色(悬停) | 蓝 | `#1da1f2` |
+| 成功 | 绿 | `#26a641` |
+| 警告 | 黄 | `#e9c46a` |
+| 错误 | 红 | `#e63946` |
+| 信息 | 紫 | `#a855f7` |
+
+**中性色(深色主题)**:
+
+| 用途 | 色值 | 说明 |
+|------|------|------|
+| 背景(最深) | `#0d1117` | 页面底色 |
+| 背景(卡片) | `#161b22` | 卡片、弹窗 |
+| 背景(悬浮) | `#21262d` | hover、选中 |
+| 边框 | `#30363d` | 分割线、边框 |
+| 文字(主) | `#e6edf3` | 标题、正文 |
+| 文字(次) | `#8b949e` | 辅助说明、时间 |
+| 文字(弱) | `#6e7681` | 占位符、disabled |
+
+### 2.2 字体与间距
+
+**字体栈**:
+```css
+font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+```
+
+**字号阶梯**:
+
+| 用途 | 大小 | 字重 |
+|------|------|------|
+| 页面大标题 | 24px | 600 |
+| 区块标题 | 18px | 600 |
+| 卡片标题 | 16px | 600 |
+| 正文 | 14px | 400 |
+| 辅助文字 | 12px | 400 |
+
+**间距阶梯**(4px 基准):
+`4px / 8px / 12px / 16px / 20px / 24px / 32px / 48px`
+
+### 2.3 圆角与阴影
+
+| 元素 | 圆角 | 阴影 |
+|------|------|------|
+| 按钮 | 6px | 无 |
+| 输入框 | 6px | 无 |
+| 卡片 | 8px | 无(用边框区分) |
+| 弹窗 | 12px | `0 8px 24px rgba(0,0,0,0.4)` |
+| 下拉菜单 | 8px | `0 4px 12px rgba(0,0,0,0.3)` |
+
+---
+
+## 三、整体布局
+
+### 3.1 布局结构
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ Top Bar(64px) │
+│ ┌──────┐ ┌──────────────┐ ┌─────────────┐ │
+│ │ Logo │ │ 项目选择器 ▼ │ │ 🔔 👤 设置 │ │
+│ └──────┘ └──────────────┘ └─────────────┘ │
+├────────┬────────────────────────────────────────────────────────┤
+│ │ │
+│ Side │ Content Area │
+│ Nav │ ┌──────────────────────────────────────────────────┐ │
+│(64px │ │ Page Header(标题 + 操作 + 筛选) │ │
+│ 展开 │ └──────────────────────────────────────────────────┘ │
+│ 220px)│ ┌──────────────────────────────────────────────────┐ │
+│ │ │ Metric Cards(指标卡片行) │ │
+│ │ └──────────────────────────────────────────────────┘ │
+│ │ ┌──────────────────────────────────────────────────┐ │
+│ │ │ Charts / Tables(主内容区) │ │
+│ │ │ │ │
+│ │ │ │ │
+│ │ └──────────────────────────────────────────────────┘ │
+│ │ │
+└────────┴────────────────────────────────────────────────────────┘
+```
+
+### 3.2 侧边栏(可折叠)
+
+**收起态(64px)**:只显示图标
+```
+📊 ← 概览
+🐛 ← 错误
+⚡ ← 性能
+🌐 ← 网络
+📈 ← 看板
+🔔 ← 告警
+⚙️ ← 设置
+```
+
+**展开态(220px)**:图标 + 文字 + 角标
+```
+📊 概览
+🐛 错误 12 ← 未解决错误数
+⚡ 性能 3 ← 有性能告警
+🌐 网络
+📈 看板
+🔔 告警 5 ← 未读告警
+⚙️ 设置
+```
+
+### 3.3 顶部栏
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ ◀▶ 🪲 Light-Sentry [▼ 前端项目 - my-app ] 🔔 ⚙️ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+- 左侧:折叠按钮 + Logo + 产品名
+- 中间:项目选择器(下拉,可搜索)
+- 右侧:告警铃(有未读红点) + 设置
+
+### 3.4 页面头(每个页面都有)
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 错误 Issues [时间范围: 24h ▼] [搜索] │
+│ 12 个未解决 · 3 个今日新增 [导出] [刷新] │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 四、核心页面详细设计
+
+### 4.1 概览页(Overview)
+
+**定位**:一屏看完项目健康状况,有问题快速跳转到对应页面
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 概览 [24h ▼] [环境: 全部 ▼] │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
+│ │ 错误数 │ │ 错误率 │ │ 影响用户│ │ LCP P95 │ │ CLS P95 │ │
+│ │ 128 │ │ 2.3% │ │ 1,234 │ │ 3.2s │ │ 0.15 │ │
+│ │ ↑15% │ │ ↑0.8% │ │ ↑12% │ │ 🔴 差 │ │ 🟡 中 │ │
+│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
+│ │
+│ ┌──────────────────────────┐ ┌───────────────────────────────┐ │
+│ │ 错误趋势(24h) │ │ Top 5 错误 │ │
+│ │ ╱╲ │ │ 1. TypeError: Cannot read... │ │
+│ │ ╱ ╲ ╱╲ │ │ 2. AxiosError: 500 Inter... │ │
+│ │ ╱╲╱ ╲╱ ╲ │ │ 3. ReferenceError: x is ... │ │
+│ │ │ │ 4. ChunkLoadError: Loadin... │ │
+│ │ │ │ 5. TypeError: Cannot set... │ │
+│ └──────────────────────────┘ └───────────────────────────────┘ │
+│ │
+│ ┌──────────────────────────┐ ┌───────────────────────────────┐ │
+│ │ 性能趋势(LCP/FID/CLS) │ │ 新错误(今日新增) │ │
+│ │ 🟢 LCP 2.5s │ │ • TypeError: Cannot read... │ │
+│ │ 🟡 FID 120ms │ │ • AxiosError: Network Er... │ │
+│ │ 🟢 CLS 0.08 │ │ • ReferenceError: foo i... │ │
+│ │ │ │ │ │
+│ └──────────────────────────┘ └───────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 实时错误流(最近 5 分钟) │ │
+│ │ 14:32:15 TypeError: Cannot read property 'foo' │ │
+│ │ 14:32:10 AxiosError: Request failed with 500 │ │
+│ │ 14:32:05 ReferenceError: x is not defined │ │
+│ │ ... │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**设计要点**:
+- 5 个核心指标卡片放在最上面,一眼看完
+- 指标卡片底部的小数字是「同比昨日」,红涨绿跌
+- 左图右表,趋势 + Top 排行
+- 底部实时流,让你感知系统状态
+
+### 4.2 错误列表页(Errors)
+
+**定位**:浏览和管理错误 Issue,找到要处理的问题
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 错误 Issues [24h ▼] [🔍 搜索错误消息] │
+│ 128 个错误 · 12 个未解决 · 3 个今日新增 [状态: 全部 ▼] [级别: ▼] │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌───┬─────────────────────┬──────┬───────┬───────┬──────────┐ │
+│ │ ✓ │ 错误消息 │ 级别 │ 次数 │ 用户 │ 最后出现 │ │
+│ ├───┼─────────────────────┼──────┼───────┼───────┼──────────┤ │
+│ │ 🔴│Cannot read property│error │ 456 │ 123 │ 2 分钟前 │ │
+│ │ │ 'foo' of undefined│ │ │ │ │ │
+│ ├───┼─────────────────────┼──────┼───────┼───────┼──────────┤ │
+│ │ 🔴│AxiosError: Request │error │ 234 │ 89 │ 5 分钟前 │ │
+│ │ │ failed with 500 │ │ │ │ │ │
+│ ├───┼─────────────────────┼──────┼───────┼───────┼──────────┤ │
+│ │ 🟡│ReferenceError: x is│warning│ 123 │ 45 │ 12 分钟前 │ │
+│ │ │ not defined │ │ │ │ │ │
+│ └───┴─────────────────────┴──────┴───────┴───────┴──────────┘ │
+│ │
+│ ◀ 1 2 3 4 ... 12 ▶ 每页 20 条 ▼ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**筛选器**:
+- 时间范围(15m / 1h / 6h / 24h / 7d / 30d / 自定义)
+- 状态(全部 / 未解决 / 已解决 / 已忽略)
+- 级别(全部 / fatal / error / warning / info)
+- 搜索(错误消息、指纹、文件路径)
+- 环境
+- 版本(release)
+
+**列表交互**:
+- 点击行 → 进入错误详情
+- 行前 checkbox → 批量操作(标记已解决、忽略)
+- hover 行 → 显示快捷操作(查看、已解决、忽略)
+
+### 4.3 错误详情页
+
+**定位**:深入分析单个错误,定位根因
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ ← 返回列表 [标记已解决] [忽略] [分配给 ▼] │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ 🔴 TypeError: Cannot read property 'foo' of undefined │
+│ Uncaught exception · active · 来自浏览器 JS │
+│ │
+│ 📊 概览 📝 堆栈 📋 Breadcrumbs 👥 用户 🌐 分布 ⏱️ 趋势 │
+│ ───── │
+│ │
+│ ┌──────────────┬──────────────┬──────────────┬──────────────┐ │
+│ │ 总次数 │ 影响用户 │ 首次出现 │ 最后出现 │ │
+│ │ 456 │ 123 │ 3 天前 │ 2 分钟前 │ │
+│ └──────────────┴──────────────┴──────────────┴──────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 发生趋势(7 天) │ │
+│ │ ╱╲ │ │
+│ │ ╱ ╲ ╱╲ │ │
+│ │ ╱╲╱ ╲╱ ╲ │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 堆栈跟踪 │ │
+│ │ 📄 app.js:123 onClick │ │
+│ │ 📄 utils.js:45 handleClick │ │
+│ │ 📄 index.js:8 main │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 最近出现(最新 5 条) │ │
+│ │ 14:32:15 user_123 Chrome 120 macOS 点击按钮时 │ │
+│ │ 14:31:45 user_456 Safari 17 iOS 加载页面时 │ │
+│ │ ... │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**Tab 内容**:
+- **概览**:核心数据 + 趋势图 + 最近出现
+- **堆栈**:完整堆栈跟踪,可折叠,点击跳源码(如果配置了 sourcemap)
+- **Breadcrumbs**:用户操作路径,时间线展示
+- **用户**:受影响的用户列表
+- **分布**:浏览器、OS、设备、地区分布
+- **趋势**:更长时间的趋势图
+
+### 4.4 性能分析页
+
+**定位**:看页面性能好不好,哪里慢
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 性能分析 [24h ▼] [页面: 全部 ▼] │
+│ Web Vitals 指标 [浏览器: 全部 ▼] │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
+│ │ LCP │ │ FID │ │ CLS │ │ FCP │ │
+│ │ 3.2s 🔴 │ │ 120ms 🟡 │ │ 0.15 🟢 │ │ 1.8s 🟡 │ │
+│ │ P95: 4.5s │ │ P95: 200ms │ │ P95: 0.25 │ │ P95: 2.5s│ │
+│ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 性能趋势(P50 / P75 / P95 / P99) │ │
+│ │ ╭────────────────────────────────────────────────────╮ │ │
+│ │ │ ▲ P99 ── P95 ─ ─ P75 ┄ ┄ P50 │ │ │
+│ │ │ │╲ │ │ │
+│ │ │ │ ╲──────╮ │ │ │
+│ │ │ │ ╲╱╲ │ │ │
+│ │ │ │ │ │ │
+│ │ │ ╰───────────────────────────────────────────────╯ │ │
+│ └───────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 页面性能排行 [按 LCP 排序 ▼] │ │
+│ ├─────────────────────┬──────┬──────┬──────┬──────┬───────┤ │
+│ │ 页面 │ LCP │ FID │ CLS │ 样本 │ 评级 │ │
+│ ├─────────────────────┼──────┼──────┼──────┼──────┼───────┤ │
+│ │ /home │ 2.8s │ 80ms │ 0.10 │ 1.2k │ 🟢 良 │ │
+│ │ /product/:id │ 4.2s │ 150ms│ 0.20 │ 800 │ 🔴 差 │ │
+│ │ /checkout │ 3.5s │ 120ms│ 0.15 │ 500 │ 🟡 中 │ │
+│ │ ... │ │ │ │ │ │ │
+│ └─────────────────────┴──────┴──────┴──────┴──────┴───────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 4.5 告警中心
+
+**定位**:配置告警规则,查看告警历史
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 告警中心 │
+│ ┌─────────┐ │
+│ │ 告警规则 │ 告警历史 │
+│ └─────────┘ │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ [+ 新建告警规则] │
+│ │
+│ ┌───┬─────────────────────┬──────────┬───────┬────────┬─────┐ │
+│ │ ✓ │ 规则名称 │ 类型 │ 级别 │ 状态 │ 操作 │ │
+│ ├───┼─────────────────────┼──────────┼───────┼────────┼─────┤ │
+│ │ 🔔│ 错误数量突增 │ 突增告警 │ 严重 │ ✅ 启用 │ ... │ │
+│ ├───┼─────────────────────┼──────────┼───────┼────────┼─────┤ │
+│ │ 🔔│ 新错误出现 │ 新错误 │ 警告 │ ✅ 启用 │ ... │ │
+│ ├───┼─────────────────────┼──────────┼───────┼────────┼─────┤ │
+│ │ 🔔│ LCP 超过 4s │ 阈值告警 │ 警告 │ ✅ 启用 │ ... │ │
+│ └───┴─────────────────────┴──────────┴───────┴────────┴─────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 五、组件设计
+
+### 5.1 指标卡片
+
+```
+┌─────────────────┐
+│ 错误总数 │ ← 标题(小字、灰色)
+│ 128 │ ← 数值(大字、粗体)
+│ ↑ 15% vs 昨日 │ ← 趋势(红涨绿跌)
+│ ╱╲ │ ← 迷你趋势图(可选)
+│ ╱ ╲ │
+└─────────────────┘
+```
+
+**变体**:
+- 标准:数字 + 趋势
+- 带评级:数字 + 颜色评级(性能指标用)
+- 极简:只有数字
+
+### 5.2 状态标签
+
+| 状态 | 颜色 | 示例 |
+|------|------|------|
+| Active(未解决) | 红底白字 | `active` |
+| Resolved(已解决) | 绿底白字 | `resolved` |
+| Ignored(已忽略) | 灰底灰字 | `ignored` |
+| Firing(告警中) | 红底白字 | `FIRING` |
+| Resolved(已恢复) | 绿底白字 | `RESOLVED` |
+
+### 5.3 错误级别标记
+
+| 级别 | 颜色 |
+|------|------|
+| Fatal | 🔴 深红 |
+| Error | 🔴 红 |
+| Warning | 🟡 黄 |
+| Info | 🔵 蓝 |
+| Debug | ⚪ 灰 |
+
+### 5.4 性能评级
+
+| 评级 | 颜色 | LCP | FID | CLS |
+|------|------|-----|-----|-----|
+| Good 🟢 | 绿 | < 2.5s | < 100ms | < 0.1 |
+| Needs Improvement 🟡 | 黄 | 2.5-4s | 100-300ms | 0.1-0.25 |
+| Poor 🔴 | 红 | > 4s | > 300ms | > 0.25 |
+
+---
+
+## 六、图表设计
+
+### 6.1 图表库选型
+
+| 库 | 体积 | 功能 | 适合场景 |
+|----|------|------|----------|
+| Chart.js | ~60KB gzip | 基础图表够用 | 轻量、简单 |
+| ECharts | ~150KB gzip | 功能强大 | 复杂图表、交互多 |
+| uPlot | ~15KB gzip | 时序图表 | 极致轻量、性能好 |
+
+**推荐方案**:
+- 默认用 Chart.js(够用、轻量、生态好)
+- 性能监控页面用 ECharts(需要更复杂的交互)
+- 可按需加载,首屏只加载必要的
+
+### 6.2 图表类型
+
+| 图表 | 用途 | 页面 |
+|------|------|------|
+| 折线图 | 错误趋势、性能趋势 | 概览、错误、性能 |
+| 柱状图 | Top N 排行、分布 | 错误、性能 |
+| 饼图 / 环形图 | 占比、分布 | 概览、错误 |
+| 面积图 | 堆叠趋势 | 性能 |
+| 热力图 | 时间段分布 | 错误 |
+
+### 6.3 与 Grafana 的关系
+
+**管理后台解决的问题**:
+- 错误 Issue 管理(状态、分配、标记)
+- 项目配置和管理
+- 告警规则配置
+- 更友好的错误详情展示
+
+**Grafana 解决的问题**:
+- 灵活的自定义仪表盘
+- 复杂的数据探索
+- 多种数据源联合查询
+- 告警(可复用 Grafana Alerting)
+
+**分工原则**:
+- 常用功能做进管理后台(开箱即用)
+- 高级分析用 Grafana(灵活强大)
+- 管理后台提供一键跳转到 Grafana 的链接
+
+---
+
+## 七、前端架构
+
+### 7.1 目录结构
+
+```
+frontend/ # 前端工程(独立目录)
+├── public/
+│ └── favicon.ico
+├── src/
+│ ├── assets/ # 静态资源
+│ │ ├── images/
+│ │ └── icons/
+│ ├── components/ # 通用组件
+│ │ ├── layout/ # 布局组件
+│ │ │ ├── MainLayout.tsx # 主布局(侧边栏+顶栏+内容)
+│ │ │ ├── Sidebar.tsx # 侧边栏
+│ │ │ ├── Header.tsx # 顶部栏
+│ │ │ └── PageHeader.tsx # 页面头
+│ │ ├── chart/ # 图表组件
+│ │ │ ├── LineChart.tsx # 折线图
+│ │ │ ├── BarChart.tsx # 柱状图
+│ │ │ └── PieChart.tsx # 饼图
+│ │ ├── metrics/ # 指标组件
+│ │ │ └── MetricCard.tsx # 指标卡片
+│ │ └── common/ # 其他通用组件
+│ │ ├── StatusTag.tsx # 状态标签
+│ │ └── CopyButton.tsx # 复制按钮
+│ ├── pages/ # 页面
+│ │ ├── overview/ # 概览页
+│ │ │ └── index.tsx
+│ │ ├── errors/ # 错误管理
+│ │ │ ├── List.tsx # 错误列表
+│ │ │ └── Detail.tsx # 错误详情
+│ │ ├── performance/ # 性能分析
+│ │ │ └── index.tsx
+│ │ ├── network/ # 网络分析
+│ │ │ └── index.tsx
+│ │ ├── alerts/ # 告警中心
+│ │ │ ├── Rules.tsx # 告警规则
+│ │ │ └── History.tsx # 告警历史
+│ │ ├── logs/ # 日志查询
+│ │ │ └── index.tsx
+│ │ ├── settings/ # 项目设置
+│ │ │ └── index.tsx
+│ │ ├── projects/ # 项目管理
+│ │ │ └── List.tsx
+│ │ └── integration/ # 接入指南
+│ │ └── index.tsx
+│ ├── store/ # 状态管理 (Zustand)
+│ │ ├── index.ts # 导出
+│ │ ├── useAppStore.ts # 全局状态(主题、侧边栏等)
+│ │ └── useProjectStore.ts # 当前项目状态
+│ ├── services/ # API 服务
+│ │ ├── request.ts # Axios 封装
+│ │ ├── project.ts # 项目相关 API
+│ │ ├── error.ts # 错误相关 API
+│ │ ├── performance.ts # 性能相关 API
+│ │ └── alert.ts # 告警相关 API
+│ ├── hooks/ # 自定义 Hooks
+│ │ ├── useProject.ts # 当前项目 hook
+│ │ ├── useTimeRange.ts # 时间范围 hook
+│ │ └── useChartTheme.ts # 图表主题 hook
+│ ├── router/ # 路由配置
+│ │ ├── index.tsx # 路由入口
+│ │ └── routes.ts # 路由表
+│ ├── utils/ # 工具函数
+│ │ ├── format.ts # 格式化(时间、数字等)
+│ │ ├── date.ts # 日期工具
+│ │ └── storage.ts # 本地存储
+│ ├── types/ # TypeScript 类型定义
+│ │ ├── api.ts # API 类型
+│ │ ├── error.ts # 错误相关类型
+│ │ ├── performance.ts # 性能相关类型
+│ │ └── common.ts # 通用类型
+│ ├── styles/ # 全局样式
+│ │ ├── global.less
+│ │ └── variables.less
+│ ├── App.tsx # 根组件
+│ └── main.tsx # 入口文件
+├── .eslintrc.js # ESLint 配置
+├── .prettierrc # Prettier 配置
+├── tsconfig.json # TypeScript 配置
+├── vite.config.ts # Vite 配置
+└── package.json
+```
+
+### 7.2 路由设计
+
+使用 React Router v6,嵌套路由 + 懒加载:
+
+```typescript
+// router/routes.ts
+const routes = [
+ {
+ path: '/',
+ element: ,
+ children: [
+ { index: true, element: },
+ { path: 'overview', lazy: () => import('@/pages/overview') },
+ {
+ path: 'errors',
+ children: [
+ { index: true, lazy: () => import('@/pages/errors/List') },
+ { path: ':id', lazy: () => import('@/pages/errors/Detail') },
+ ],
+ },
+ { path: 'performance', lazy: () => import('@/pages/performance') },
+ { path: 'network', lazy: () => import('@/pages/network') },
+ {
+ path: 'alerts',
+ children: [
+ { index: true, element: },
+ { path: 'rules', lazy: () => import('@/pages/alerts/Rules') },
+ { path: 'history', lazy: () => import('@/pages/alerts/History') },
+ ],
+ },
+ { path: 'logs', lazy: () => import('@/pages/logs') },
+ { path: 'settings', lazy: () => import('@/pages/settings') },
+ { path: 'integration', lazy: () => import('@/pages/integration') },
+ ],
+ },
+ { path: '/projects', element: },
+ { path: '*', element: },
+];
+```
+
+### 7.3 状态管理(Zustand)
+
+**为什么用 Zustand 而不是 Redux?**
+- 比 Redux 简单太多,没有 reducer、action、dispatch 那些概念
+- 体积小(~1KB),性能好
+- TypeScript 支持好
+- 支持 middleware(persist、devtools 等)
+
+```typescript
+// store/useAppStore.ts
+import { create } from 'zustand';
+
+interface AppState {
+ theme: 'dark' | 'light';
+ sidebarCollapsed: boolean;
+ currentProject: Project | null;
+
+ toggleTheme: () => void;
+ toggleSidebar: () => void;
+ setCurrentProject: (project: Project) => void;
+}
+
+export const useAppStore = create((set) => ({
+ theme: 'dark',
+ sidebarCollapsed: false,
+ currentProject: null,
+
+ toggleTheme: () => set((s) => ({ theme: s.theme === 'dark' ? 'light' : 'dark' })),
+ toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
+ setCurrentProject: (project) => set({ currentProject: project }),
+}));
+```
+
+### 7.4 数据请求(TanStack Query)
+
+**为什么用 TanStack Query?**
+- 自动缓存,相同请求不会重复发
+- 后台自动刷新
+- 分页、无限滚动都有封装
+- 乐观更新、重试策略
+- 和 Zustand 互补,不用把 API 数据放全局 store
+
+```typescript
+// hooks/useErrors.ts
+import { useQuery } from '@tanstack/react-query';
+import { getErrorList } from '@/services/error';
+
+export function useErrorList(params: ErrorListParams) {
+ return useQuery({
+ queryKey: ['errors', params],
+ queryFn: () => getErrorList(params),
+ keepPreviousData: true,
+ staleTime: 30_000, // 30秒内认为是新鲜的
+ });
+}
+```
+
+### 7.5 请求封装(Axios)
+
+```typescript
+// services/request.ts
+import axios from 'axios';
+import { message } from 'antd';
+
+const request = axios.create({
+ baseURL: '/api',
+ timeout: 15000,
+});
+
+// 请求拦截器:加 token
+request.interceptors.request.use((config) => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+});
+
+// 响应拦截器:统一处理错误
+request.interceptors.response.use(
+ (res) => res.data,
+ (err) => {
+ const status = err.response?.status;
+ const message = err.response?.data?.message || err.message;
+
+ if (status === 401) {
+ // 登录过期,跳登录页
+ } else {
+ message.error(message);
+ }
+
+ return Promise.reject(err);
+ }
+);
+
+export default request;
+```
+
+### 7.6 主题配置(Ant Design 5 深色主题)
+
+Ant Design 5 用 CSS-in-JS,通过 ConfigProvider 配置主题:
+
+```typescript
+// App.tsx
+import { ConfigProvider, theme as antdTheme } from 'antd';
+import { useAppStore } from '@/store';
+
+function App() {
+ const { theme } = useAppStore();
+
+ return (
+
+
+
+ );
+}
+```
+
+**自定义暗色主题色板**(贴合 GitHub Dark 风格):
+```typescript
+{
+ colorBgLayout: '#0d1117', // 页面背景
+ colorBgContainer: '#161b22', // 卡片背景
+ colorBgElevated: '#21262d', // 悬浮背景
+ colorBorder: '#30363d', // 边框
+ colorText: '#e6edf3', // 主文字
+ colorTextSecondary: '#8b949e',// 次文字
+ colorTextTertiary: '#6e7681', // 弱文字
+}
+```
+
+### 7.7 图表封装
+
+基于 ECharts 封装通用图表组件,自动适配主题和容器大小:
+
+```typescript
+// components/chart/BaseChart.tsx
+import { useEffect, useRef } from 'react';
+import * as echarts from 'echarts';
+import { useAppStore } from '@/store';
+
+interface BaseChartProps {
+ option: echarts.EChartsOption;
+ height?: number | string;
+ className?: string;
+}
+
+export function BaseChart({ option, height = 300, className }: BaseChartProps) {
+ const chartRef = useRef(null);
+ const chartInstance = useRef(null);
+ const { theme } = useAppStore();
+
+ useEffect(() => {
+ if (!chartRef.current) return;
+
+ chartInstance.current = echarts.init(
+ chartRef.current,
+ theme === 'dark' ? 'dark' : null
+ );
+
+ const resizeObserver = new ResizeObserver(() => {
+ chartInstance.current?.resize();
+ });
+ resizeObserver.observe(chartRef.current);
+
+ return () => {
+ resizeObserver.disconnect();
+ chartInstance.current?.dispose();
+ };
+ }, [theme]);
+
+ useEffect(() => {
+ chartInstance.current?.setOption(option, true);
+ }, [option]);
+
+ return ;
+}
+```
+
+### 7.8 构建与部署
+
+**开发环境**:
+```bash
+pnpm dev # 启动开发服务器
+pnpm build # 生产构建
+pnpm preview # 预览构建结果
+pnpm lint # ESLint 检查
+pnpm type-check # TypeScript 类型检查
+```
+
+**部署方案**:
+- 构建产物:`dist/` 目录
+- Nginx 直接托管静态文件
+- API 请求通过 Nginx 反向代理到后端
+- 和现有 `public/manage/` 共存,新前端走 `/manage/` 路径
+
+```nginx
+location /manage/ {
+ try_files $uri $uri/ /manage/index.html;
+ root /path/to/frontend/dist;
+}
+```
+
+---
+
+## 八、交互设计原则
+
+### 8.1 快速导航
+
+- 全局搜索:`⌘K` 唤起,可搜项目、错误、页面
+- 面包屑:永远知道自己在哪
+- 相关跳转:错误详情里可跳转到对应页面的性能分析
+
+### 8.2 数据加载
+
+- 骨架屏:数据加载中显示骨架,不跳
+- 增量加载:列表滚动加载
+- 实时刷新:可开/关,默认 30 秒自动刷新
+
+### 8.3 操作反馈
+
+- 重要操作(删除、标记已解决)有确认弹窗
+- 成功操作顶部弹 toast,3 秒自动消失
+- 操作失败显示错误原因,可重试
+
+---
+
+## 九、API 设计
+
+### 9.1 统计 API
+
+```
+# 概览数据
+GET /api/projects/{id}/overview?time_range=24h
+
+# 错误趋势
+GET /api/projects/{id}/errors/trend?time_range=24h&interval=1h
+
+# 错误列表
+GET /api/projects/{id}/errors?page=1&page_size=20&level=error&status=active
+
+# 错误详情
+GET /api/projects/{id}/errors/{fingerprint}
+
+# 性能指标
+GET /api/projects/{id}/performance/metrics?time_range=24h&metric=lcp
+
+# 告警规则
+GET /api/projects/{id}/alerts/rules
+POST /api/projects/{id}/alerts/rules
+PUT /api/projects/{id}/alerts/rules/{rule_id}
+DELETE /api/projects/{id}/alerts/rules/{rule_id}
+
+# 告警历史
+GET /api/projects/{id}/alerts/history?page=1&page_size=20
+```
+
+---
+
+## 十、演进路线
+
+### 10.1 当前状态
+
+现有的 [index.html](file:///Users/weidingjian/Desktop/work/ai/light-sentry/public/manage/index.html) 是项目列表页,只有最基础的 CRUD 功能。
+
+### 10.2 版本规划
+
+**v1.1(下一个版本)**:
+- 改造成侧边栏 + 内容区的布局
+- 新增概览页(核心指标 + 错误趋势)
+- 新增错误列表页(从聚合表读)
+- 项目选择器移到顶部
+
+**v1.2**:
+- 错误详情页
+- 性能分析页
+- 告警规则管理
+
+**v2.0**:
+- 完整的错误管理工作流
+- 性能深入分析(瀑布图、资源时序)
+- 用户会话追踪
diff --git a/docs/06-deployment.md b/docs/06-deployment.md
new file mode 100644
index 0000000..104709e
--- /dev/null
+++ b/docs/06-deployment.md
@@ -0,0 +1,544 @@
+# 部署与运维设计文档
+
+## 一、部署方式
+
+### 1.1 Docker Compose(推荐)
+
+一键部署,适合中小团队。
+
+```
+docker compose up -d
+```
+
+包含的服务:
+
+| 服务 | 镜像 | 端口 | 说明 |
+|------|------|------|------|
+| light-sentry-api | 自建 (Node.js) | 9000 | API 服务 + 管理后台 |
+| loki | grafana/loki | 3100 | 日志存储 |
+| promtail | grafana/promtail | - | 日志收集(容器日志) |
+| grafana | grafana/grafana | 3000 | 数据可视化 |
+| dozzle | amir20/dozzle | 8080 | 容器日志查看 |
+| nginx | (系统已有) | 80/443 | 反向代理 + SSL |
+
+### 1.2 二进制部署
+
+适合对 Docker 不熟悉的场景:
+
+1. 安装 Node.js 18+
+2. 安装 Loki(单二进制)
+3. 安装 Grafana
+4. 配置 Nginx
+5. 启动 API 服务(pm2 守护)
+
+### 1.3 Kubernetes 部署
+
+适合大规模、多租户场景:
+
+- Helm Chart 部署
+- HPA 自动扩缩容
+- Loki 分布式模式
+- MySQL / PostgreSQL 集群
+
+---
+
+## 二、资源需求
+
+### 2.1 最小配置(测试/个人用)
+
+| 资源 | 配置 | 说明 |
+|------|------|------|
+| CPU | 1 核 | 低流量下足够 |
+| 内存 | 1GB | 紧张,可能需要调小 Loki 缓存 |
+| 磁盘 | 20GB | 存 7 天日志(每天 ~2GB) |
+| 网络 | 1Mbps | 上报流量不大 |
+
+**承载能力**:
+- 10 个项目以内
+- 每天 1 万事件以内
+
+### 2.2 推荐配置(小团队)
+
+| 资源 | 配置 | 说明 |
+|------|------|------|
+| CPU | 2 核 | 应对突发流量 |
+| 内存 | 4GB | Loki + API + Grafana 都够 |
+| 磁盘 | 100GB SSD | 存 30 天日志 |
+| 网络 | 5Mbps | 足够 |
+
+**承载能力**:
+- 50 个项目以内
+- 每天 100 万事件以内
+
+### 2.3 生产配置(中大型团队)
+
+| 资源 | 配置 | 说明 |
+|------|------|------|
+| CPU | 4 核以上 | 高并发上报 |
+| 内存 | 8GB 以上 | Loki 内存索引 |
+| 磁盘 | 500GB+ SSD | 大量日志存储 |
+| 网络 | 10Mbps+ | 大流量上报 |
+
+**承载能力**:
+- 200 个项目以内
+- 每天 1000 万事件以内
+
+---
+
+## 三、Docker Compose 配置详解
+
+### 3.1 完整配置说明
+
+```yaml
+version: '3.8'
+
+services:
+ # API 服务 + 管理后台
+ light-sentry-api:
+ build: .
+ container_name: light-sentry-api
+ network_mode: host
+ environment:
+ - NODE_ENV=production
+ - PORT=9000
+ - LOKI_URL=http://127.0.0.1:3100
+ - DATA_DIR=/app/data
+ volumes:
+ - ./data:/app/data
+ restart: unless-stopped
+ depends_on:
+ - loki
+
+ # Loki - 日志存储
+ loki:
+ image: grafana/loki:2.9.4
+ container_name: light-sentry-loki
+ network_mode: host
+ command: -config.file=/etc/loki/local-config.yaml
+ volumes:
+ - ./loki/config:/etc/loki
+ - loki-data:/loki
+ restart: unless-stopped
+
+ # Promtail - 收集容器日志
+ promtail:
+ image: grafana/promtail:2.9.4
+ container_name: light-sentry-promtail
+ network_mode: host
+ volumes:
+ - ./promtail/config:/etc/promtail
+ - /var/lib/docker/containers:/var/lib/docker/containers:ro
+ restart: unless-stopped
+ depends_on:
+ - loki
+
+ # Grafana - 可视化
+ grafana:
+ image: grafana/grafana:10.2.0
+ container_name: light-sentry-grafana
+ network_mode: host
+ environment:
+ - GF_SECURITY_ADMIN_PASSWORD=admin123
+ volumes:
+ - grafana-data:/var/lib/grafana
+ restart: unless-stopped
+
+ # Dozzle - 容器日志查看
+ dozzle:
+ image: amir20/dozzle:latest
+ container_name: light-sentry-dozzle
+ network_mode: host
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ restart: unless-stopped
+
+volumes:
+ loki-data:
+ grafana-data:
+```
+
+### 3.2 Loki 配置
+
+`loki/config/local-config.yaml`
+
+```yaml
+auth_enabled: false
+
+server:
+ http_listen_port: 3100
+
+ingester:
+ lifecycler:
+ ring:
+ kvstore:
+ store: inmemory
+ replication_factor: 1
+ chunk_idle_period: 30m
+ chunk_retain_period: 1m
+ max_transfer_retries: 0
+
+schema_config:
+ configs:
+ - from: 2024-01-01
+ store: boltdb-shipper
+ object_store: filesystem
+ schema: v12
+ index:
+ prefix: index_
+ period: 24h
+
+storage_config:
+ boltdb_shipper:
+ active_index_directory: /loki/index
+ cache_location: /loki/index_cache
+ shared_store: filesystem
+ filesystem:
+ directory: /loki/chunks
+
+limits_config:
+ retention_period: 168h # 7 天
+ per_stream_rate_limit: 10MB
+ ingestion_rate_mb: 50
+ ingestion_burst_size_mb: 100
+ max_streams_per_user: 10000
+ max_chunks_per_query: 2000000
+ max_query_series: 5000
+
+table_manager:
+ retention_deletes_enabled: true
+ retention_period: 168h # 7 天
+```
+
+---
+
+## 四、Nginx 配置
+
+### 4.1 反向代理配置
+
+```nginx
+server {
+ listen 443 ssl http2;
+ server_name log.example.com;
+
+ # SSL 配置
+ ssl_certificate /path/to/fullchain.pem;
+ ssl_certificate_key /path/to/privkey.pem;
+
+ # API 服务(Sentry 上报 + 管理后台 API)
+ location /api/ {
+ proxy_pass http://127.0.0.1:9000/api/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # 上报接口超时
+ proxy_read_timeout 30s;
+ proxy_connect_timeout 10s;
+ proxy_send_timeout 30s;
+
+ # CORS(SDK 上报需要)
+ add_header Access-Control-Allow-Origin * always;
+ add_header Access-Control-Allow-Methods GET,POST,OPTIONS always;
+ add_header Access-Control-Allow-Headers Content-Type,X-Sentry-Auth always;
+ if ($request_method = OPTIONS) {
+ return 204;
+ }
+ }
+
+ # 管理后台
+ location /manage/ {
+ proxy_pass http://127.0.0.1:9000/manage/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # Grafana
+ location /grafana/ {
+ proxy_pass http://127.0.0.1:3000/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ # Dozzle
+ location /dozzle/ {
+ proxy_pass http://127.0.0.1:8080/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket 支持
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # Loki(可选,直接暴露给 Grafana 用,一般不需要外部访问)
+ location /loki/ {
+ proxy_pass http://127.0.0.1:3100/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ deny all; # 禁止外部访问,只允许内网
+ }
+
+ # 根路径重定向到管理后台
+ location = / {
+ return 301 /manage/;
+ }
+}
+```
+
+### 4.2 限流配置
+
+```nginx
+# 定义限流 zone
+limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
+
+location /api/ {
+ limit_req zone=api_limit burst=200 nodelay;
+ # ... 其他配置
+}
+```
+
+---
+
+## 五、CI/CD 部署
+
+### 5.1 Gitea Actions 示例
+
+```yaml
+name: Deploy Light-Sentry
+
+on:
+ push:
+ branches: [ main ]
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Deploy via SSH
+ uses: appleboy/ssh-action@v1.0.0
+ with:
+ host: ${{ secrets.SERVER_HOST }}
+ username: ${{ secrets.SERVER_USER }}
+ key: ${{ secrets.SSH_PRIVATE_KEY }}
+ script: |
+ cd ~/light-sentry
+ git pull
+ docker compose build light-sentry-api
+ docker compose up -d
+ echo "Deploy completed"
+```
+
+### 5.2 智能重启(只重启变更的服务)
+
+检测变更的文件,只重启相关服务,减少停机时间:
+
+- `src/**` → 重启 API 服务
+- `loki/**` → 重启 Loki
+- `grafana/**` → 重启 Grafana
+- `docker-compose.yml` → 全部重启
+- `nginx/**` → reload Nginx
+
+---
+
+## 六、监控与告警
+
+### 6.1 自身监控
+
+Light-Sentry 自己也要监控自己:
+
+| 监控项 | 方式 | 告警阈值 |
+|--------|------|----------|
+| API 服务存活 | 健康检查接口 + Prometheus | 502 持续 1 分钟 |
+| Loki 服务存活 | Grafana 内置监控 | 连接失败 |
+| 磁盘使用率 | Node Exporter | > 80% 告警 |
+| 内存使用率 | Node Exporter | > 85% 告警 |
+| CPU 使用率 | Node Exporter | > 90% 告警 |
+| 事件上报量 | Loki 查询 | 突增 200% |
+| 错误率 | Loki 查询 | > 5% |
+
+### 6.2 健康检查
+
+```
+GET /health
+
+{
+ "status": "ok",
+ "timestamp": "2024-01-01T00:00:00Z",
+ "uptime": 86400,
+ "queue_size": 123,
+ "loki": "connected",
+ "mysql": "connected"
+}
+```
+
+---
+
+## 七、备份与恢复
+
+### 7.1 需要备份的数据
+
+| 数据 | 位置 | 备份频率 | 保留时间 |
+|------|------|----------|----------|
+| 项目配置 | data/projects.json | 每天 | 30 天 |
+| Loki 数据 | loki-data 卷 | 每周 | 4 周 |
+| Grafana 配置 | grafana-data 卷 | 每天 | 30 天 |
+| 告警规则 | MySQL | 每天 | 永久 |
+| 错误聚合 | MySQL | 每天 | 永久 |
+
+### 7.2 备份脚本
+
+```bash
+#!/bin/bash
+# backup.sh - Light-Sentry 备份脚本
+
+BACKUP_DIR="/backup/light-sentry"
+DATE=$(date +%Y%m%d_%H%M%S)
+
+# 创建备份目录
+mkdir -p $BACKUP_DIR
+
+# 备份配置文件
+tar czf $BACKUP_DIR/config_$DATE.tar.gz /opt/light-sentry/data/
+
+# 备份 Loki 数据(可选,量大)
+# docker run --rm -v loki-data:/data -v $BACKUP_DIR:/backup \
+# alpine tar czf /backup/loki_$DATE.tar.gz -C /data .
+
+# 备份 Grafana
+docker run --rm -v grafana-data:/data -v $BACKUP_DIR:/backup \
+ alpine tar czf /backup/grafana_$DATE.tar.gz -C /data .
+
+# 清理 30 天前的备份
+find $BACKUP_DIR -name "*.tar.gz" -mtime +30 -delete
+
+echo "Backup completed: $DATE"
+```
+
+### 7.3 恢复流程
+
+```bash
+# 1. 停止服务
+docker compose down
+
+# 2. 恢复配置
+tar xzf backup/config_20240101_000000.tar.gz -C /opt/light-sentry/
+
+# 3. 恢复 Grafana
+docker run --rm -v grafana-data:/data -v /backup:/backup \
+ alpine tar xzf /backup/grafana_20240101_000000.tar.gz -C /data
+
+# 4. 启动服务
+docker compose up -d
+```
+
+---
+
+## 八、性能优化
+
+### 8.1 API 服务优化
+
+- **Node.js 集群模式**:利用多核 CPU
+- **内存队列调优**:根据流量调整队列大小和刷盘频率
+- **连接池**:数据库连接池、HTTP 连接池
+- **缓存**:项目配置缓存,不用每次查存储
+
+### 8.2 Loki 优化
+
+- **调整保留时间**:从 7 天调整到实际需要的时间
+- **降低索引基数**:减少 label 数量
+- **批量写入**:增加 chunk 大小,减少写入次数
+- **内存调优**:根据实际数据量调整内存
+
+### 8.3 Nginx 优化
+
+- **启用 gzip 压缩**:减少传输体积
+- **启用缓存**:静态文件缓存
+- **TCP 参数调优**:增加连接数限制
+
+---
+
+## 九、安全加固
+
+### 9.1 网络安全
+
+- 管理后台加访问密码(HTTP Basic Auth 或登录)
+- Loki 不对外暴露(只允许 Grafana 访问)
+- Dozzle 加访问密码
+- 只开放必要端口(80/443)
+
+### 9.2 数据安全
+
+- SDK 上报接口 CORS 限制域名(可选)
+- 项目 DSN 泄露后可重置 publicKey
+- 敏感数据自动脱敏
+- 定期备份,异地存储
+
+### 9.3 依赖安全
+
+- 定期更新依赖
+- 镜像扫描漏洞
+- 最小权限运行
+
+---
+
+## 十、常见问题排查
+
+### 10.1 事件上报失败
+
+1. 检查网络是否通:`curl -I https://log.example.com/api/1001/envelope/`
+2. 检查 DSN 是否正确:publicKey 和 projectId 对应
+3. 查看 API 日志:`docker logs light-sentry-api`
+4. 查看 Nginx 日志:`tail -f /var/log/nginx/access.log`
+
+### 10.2 Grafana 查不到数据
+
+1. 检查 Loki 数据源是否配置正确
+2. 检查时间范围是否正确
+3. 检查 label 拼写是否正确
+4. 查看 Loki 日志:`docker logs light-sentry-loki`
+
+### 10.3 服务无法启动
+
+1. 检查端口是否被占用:`netstat -tlnp | grep 9000`
+2. 查看容器日志:`docker logs light-sentry-api`
+3. 检查配置文件格式:JSON / YAML 语法
+4. 检查磁盘空间:`df -h`
+
+---
+
+## 十一、升级策略
+
+### 11.1 版本升级
+
+```bash
+# 拉取最新代码
+git pull
+
+# 重新构建并启动
+docker compose build light-sentry-api
+docker compose up -d
+
+# 验证
+curl https://log.example.com/health
+```
+
+### 11.2 数据迁移
+
+- 配置向后兼容,新版本能读旧版本数据
+- 启动时自动检测并执行数据迁移
+- 迁移前自动备份
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..2df7f86
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,577 @@
+# Light-Sentry 日志分析系统设计文档
+
+## 1. 项目概述
+
+### 1.1 背景
+基于用户需求,需要构建一个日志分析系统,支持接收 Sentry 前后端 SDK 的上报,并在 **2核2G ECS + 阿里云OSS** 的资源限制下稳定运行。
+
+### 1.2 核心目标
+- **兼容性**:完全兼容 Sentry SDK 的上报协议(DSN、事件格式)
+- **轻量级**:适配 2核2G 服务器资源
+- **低成本**:利用阿里云 OSS 进行冷存储,降低成本
+- **可视化**:提供友好的日志查询和可视化界面
+
+---
+
+## 2. 系统架构
+
+### 2.1 整体架构图
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ 客户端 SDK 层 │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ Browser SDK │ │ Node SDK │ │ Python SDK │ │
+│ │ @sentry/browser │ @sentry/node │ @sentry/python │ │
+│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
+│ │ │ │ │
+│ └─────────────────┼─────────────────┘ │
+│ ▼ │
+│ ┌───────────────────────┐ │
+│ │ Sentry API 网关 │ │
+│ │ /api/{project}/store │ │
+│ └───────────┬───────────┘ │
+└───────────────────────────┼─────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ 服务端处理层 │
+│ ┌──────────────────┐ ┌──────────────────┐ │
+│ │ 事件处理器 │──▶│ 事件转换器 │ │
+│ │ (Event Handler)│ │ (Event Converter)│ │
+│ └──────────────────┘ └──────────┬───────┘ │
+│ │ │
+│ ┌───────────────────────┼───────────────────────┐ │
+│ ▼ ▼ ▼ │
+│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
+│ │ Loki 索引 │ │ Loki 存储 │ │ OSS 持久化 │ │
+│ │ (索引元数据) │ │ (近期日志) │ │ (历史归档) │ │
+│ └───────────────┘ └───────────────┘ └───────────────┘ │
+└─────────────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│ 可视化查询层 │
+│ ┌───────────────┐ │
+│ │ Grafana │ │
+│ │ (查询/图表) │ │
+│ └───────────────┘ │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 核心组件说明
+
+| 组件 | 技术选型 | 角色 | 资源占用 |
+|------|---------|------|---------|
+| **API网关** | Express/FastAPI | 接收Sentry协议上报 | ~100MB |
+| **日志存储** | Loki | 索引和近期日志存储 | ~500MB |
+| **持久化层** | 阿里云OSS | 历史日志归档 | 按需扩展 |
+| **可视化** | Grafana | 日志查询和图表展示 | ~300MB |
+
+**总内存预估**:~1GB(保留1GB给系统和缓冲)
+
+---
+
+## 3. 技术方案
+
+### 3.1 Sentry 协议兼容层
+
+#### 3.1.1 协议说明
+
+Sentry SDK 使用 DSN (Data Source Name) 配置上报地址:
+```
+{PROTOCOL}://{PUBLIC_KEY}@{HOST}/{PATH}/{PROJECT_ID}
+```
+
+SDK 向 `/api/{PROJECT_ID}/store/` 端点发送 POST 请求,数据格式为 JSON。
+
+#### 3.1.2 事件数据结构(简化版)
+
+```json
+{
+ "event_id": "abc123...",
+ "timestamp": "2024-01-01T00:00:00Z",
+ "level": "error",
+ "logger": "javascript",
+ "platform": "javascript",
+ "message": "Uncaught TypeError: Cannot read property",
+ "exception": {
+ "values": [{
+ "type": "TypeError",
+ "value": "Cannot read property 'x' of undefined",
+ "stacktrace": {
+ "frames": [{
+ "filename": "app.js",
+ "lineno": 42,
+ "function": "doSomething"
+ }]
+ }
+ }]
+ },
+ "tags": {
+ "environment": "production",
+ "release": "v1.0.0"
+ },
+ "contexts": {
+ "request": {
+ "url": "https://example.com/page",
+ "method": "GET"
+ },
+ "user": {
+ "id": "12345",
+ "email": "user@example.com"
+ }
+ }
+}
+```
+
+#### 3.1.3 API 网关设计
+
+**技术选型**:Node.js + Express(轻量级,内存占用低)
+
+**核心端点**:
+
+| 端点 | 方法 | 功能 |
+|------|------|------|
+| `/api/:projectId/store/` | POST | 接收 Sentry SDK 上报事件 |
+| `/health` | GET | 健康检查 |
+
+**设计要点**:
+- 支持 Sentry 的 DSN 认证(通过 `X-Sentry-Auth` 头或查询参数)
+- 异步处理,不阻塞请求响应
+- 事件格式校验和标准化
+- 支持批量上报
+
+### 3.2 日志存储方案
+
+#### 3.2.1 Loki 配置
+
+**为什么选择 Loki**:
+- 轻量级,内存占用远低于 ELK(Elasticsearch 需要至少 4GB)
+- 基于标签索引,查询效率高
+- 与 Grafana 无缝集成
+- 支持 OSS 作为后端存储
+
+**关键配置**:
+
+```yaml
+auth_enabled: false
+
+server:
+ http_listen_port: 3100
+
+ingester:
+ lifecycler:
+ address: 127.0.0.1
+ ring:
+ kvstore:
+ store: inmemory
+ replication_factor: 1
+ final_sleep: 0s
+ chunk_idle_period: 1h
+ chunk_retain_period: 30s
+ max_transfer_retries: 0
+
+schema_config:
+ configs:
+ - from: 2024-01-01
+ store: boltdb-shipper
+ object_store: aws
+ schema: v11
+ index:
+ prefix: loki_index_
+ period: 24h
+
+storage_config:
+ aws:
+ s3: s3://{AK}:{SK}@{REGION}/{BUCKET}
+ s3forcepathstyle: true
+ boltdb_shipper:
+ active_index_directory: /data/loki/index
+ cache_location: /data/loki/cache
+ shared_store: s3
+
+limits_config:
+ reject_old_samples: true
+ reject_old_samples_max_age: 168h
+
+chunk_store_config:
+ max_look_back_period: 0s
+
+table_manager:
+ retention_deletes_enabled: true
+ retention_period: 30d
+```
+
+#### 3.2.2 OSS 存储策略
+
+**存储层级**:
+
+| 层级 | 用途 | 存储类型 | 成本 |
+|------|------|---------|------|
+| **标准存储** | 近期日志(30天内) | OSS标准 | 0.12元/GB/月 |
+| **低频存储** | 中期日志(30-90天) | OSS低频 | 0.07元/GB/月 |
+| **归档存储** | 历史日志(90天以上) | OSS归档 | 0.014元/GB/月 |
+
+**生命周期管理**:
+```xml
+
+
+ transition-to-infrequent-access
+
+ loki/
+
+ Enabled
+
+ 30
+ IA
+
+
+ 90
+ Archive
+
+
+ 365
+
+
+
+```
+
+### 3.3 事件处理流程
+
+```
+SDK上报 → API网关 → 格式校验 → 标签提取 → Loki写入 → OSS持久化
+ ↓
+ 索引元数据存储
+```
+
+**标签提取策略**(用于 Loki 查询):
+
+| 标签名 | 来源字段 | 用途 |
+|--------|---------|------|
+| `project` | `project_id` | 项目隔离 |
+| `environment` | `tags.environment` | 环境区分 |
+| `level` | `level` | 日志级别过滤 |
+| `platform` | `platform` | 平台分类 |
+| `release` | `tags.release` | 版本追踪 |
+| `logger` | `logger` | 日志来源 |
+
+### 3.4 可视化方案
+
+#### 3.4.1 Grafana 配置
+
+**数据源配置**:
+- 类型:Loki
+- URL:`http://localhost:3100`
+- 认证:无
+
+**查询示例**:
+
+```logql
+// 查询所有 error 级别日志
+{level="error"} |= "error"
+
+// 查询特定项目的日志
+{project="my-project", environment="production"}
+
+// 查询包含特定错误信息的日志
+{level="error"} |= "TypeError"
+
+// 统计错误数量
+count_over_time({level="error"}[1m])
+```
+
+**仪表盘设计**:
+
+| 面板 | 功能 | 查询 |
+|------|------|------|
+| 错误趋势 | 实时错误数量折线图 | `count_over_time({level="error"}[1m])` |
+| 环境分布 | 各环境错误占比饼图 | `sum(count_over_time({level="error"}[1h])) by (environment)` |
+| 平台分布 | 各平台错误占比饼图 | `sum(count_over_time({level="error"}[1h])) by (platform)` |
+| 最新错误 | 最近错误列表 | `{level="error"} | tail 10` |
+| Top错误类型 | 错误类型排名 | `topk(5, count by (exception_type) (count_over_time({level="error"}[1h])))` |
+
+---
+
+## 4. 部署方案
+
+### 4.1 服务器配置
+
+**ECS 规格**:2核2G(阿里云 ecs.g6.large 或同等规格)
+
+**操作系统**:Ubuntu 22.04 LTS
+
+**磁盘配置**:
+- 系统盘:40GB SSD
+- 数据盘:20GB SSD(用于 Loki 索引缓存)
+
+### 4.2 资源分配
+
+| 组件 | CPU | 内存 |
+|------|-----|------|
+| 系统预留 | 0.5核 | 512MB |
+| API网关 (Node.js) | 0.5核 | 256MB |
+| Loki | 1核 | 768MB |
+| Grafana | 0.5核 | 512MB |
+
+### 4.3 Docker Compose 部署
+
+```yaml
+version: '3.8'
+
+services:
+ light-sentry-api:
+ image: light-sentry/api:latest
+ ports:
+ - "9000:9000"
+ environment:
+ - LOKI_URL=http://loki:3100
+ - OSS_ENDPOINT=${OSS_ENDPOINT}
+ - OSS_ACCESS_KEY=${OSS_ACCESS_KEY}
+ - OSS_SECRET_KEY=${OSS_SECRET_KEY}
+ - OSS_BUCKET=${OSS_BUCKET}
+ depends_on:
+ - loki
+ restart: unless-stopped
+ deploy:
+ resources:
+ limits:
+ cpus: '0.5'
+ memory: 256M
+
+ loki:
+ image: grafana/loki:2.9.0
+ ports:
+ - "3100:3100"
+ volumes:
+ - ./loki/config:/etc/loki
+ - ./loki/data:/data/loki
+ environment:
+ - AWS_ACCESS_KEY_ID=${OSS_ACCESS_KEY}
+ - AWS_SECRET_ACCESS_KEY=${OSS_SECRET_KEY}
+ restart: unless-stopped
+ deploy:
+ resources:
+ limits:
+ cpus: '1'
+ memory: 768M
+
+ grafana:
+ image: grafana/grafana:10.2.0
+ ports:
+ - "3000:3000"
+ volumes:
+ - ./grafana/data:/var/lib/grafana
+ - ./grafana/provisioning:/etc/grafana/provisioning
+ environment:
+ - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
+ depends_on:
+ - loki
+ restart: unless-stopped
+ deploy:
+ resources:
+ limits:
+ cpus: '0.5'
+ memory: 512M
+```
+
+### 4.4 Nginx 反向代理
+
+```nginx
+server {
+ listen 80;
+ server_name logs.example.com;
+
+ location /api/ {
+ proxy_pass http://localhost:9000/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ client_max_body_size 10m;
+ }
+
+ location /grafana/ {
+ proxy_pass http://localhost:3000/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+}
+```
+
+---
+
+## 5. SDK 接入指南
+
+### 5.1 前端 SDK 配置
+
+```javascript
+import * as Sentry from '@sentry/browser';
+
+Sentry.init({
+ dsn: 'https://public_key@logs.example.com/api/1/store/',
+ environment: 'production',
+ release: 'v1.0.0',
+ tracesSampleRate: 1.0,
+});
+```
+
+### 5.2 后端 SDK 配置(Node.js)
+
+```javascript
+import * as Sentry from '@sentry/node';
+
+Sentry.init({
+ dsn: 'https://public_key@logs.example.com/api/2/store/',
+ environment: 'production',
+ release: 'v1.0.0',
+ tracesSampleRate: 1.0,
+});
+```
+
+### 5.3 后端 SDK 配置(Python)
+
+```python
+import sentry_sdk
+
+sentry_sdk.init(
+ dsn="https://public_key@logs.example.com/api/3/store/",
+ environment="production",
+ release="v1.0.0",
+ traces_sample_rate=1.0,
+)
+```
+
+---
+
+## 6. 性能优化策略
+
+### 6.1 内存优化
+
+| 策略 | 说明 |
+|------|------|
+| Loki 使用 OSS 作为后端 | 减少本地磁盘和内存占用 |
+| API网关使用流处理 | 避免一次性加载大请求 |
+| 限制并发连接数 | 使用 Nginx 限流 |
+
+### 6.2 存储优化
+
+| 策略 | 说明 |
+|------|------|
+| OSS 生命周期管理 | 自动降级存储类型 |
+| Loki 压缩 | 启用 Snappy 压缩 |
+| 日志采样 | 高频日志采样处理 |
+
+### 6.3 查询优化
+
+| 策略 | 说明 |
+|------|------|
+| 标签索引 | 使用标签过滤而非全文搜索 |
+| 查询缓存 | Grafana 缓存查询结果 |
+| 时间范围限制 | 默认查询最近24小时 |
+
+---
+
+## 7. 监控与告警
+
+### 7.1 健康检查
+
+| 检查项 | 端点 | 频率 |
+|--------|------|------|
+| API网关 | `/health` | 30秒 |
+| Loki | `/ready` | 30秒 |
+| Grafana | `/api/health` | 30秒 |
+
+### 7.2 告警规则
+
+| 告警项 | 条件 | 通知方式 |
+|--------|------|---------|
+| API错误率 | 错误率 > 5% | 钉钉/邮件 |
+| 服务不可用 | 健康检查失败 | 钉钉/邮件 |
+| 内存使用率 | 内存 > 85% | 钉钉/邮件 |
+| OSS存储告警 | 存储 > 80% | 阿里云告警 |
+
+---
+
+## 8. 安全策略
+
+### 8.1 认证授权
+
+- API 网关支持 Sentry DSN 认证
+- Grafana 启用基础认证
+- Nginx 配置访问控制列表
+
+### 8.2 数据加密
+
+- 传输层:HTTPS/TLS 1.2+
+- 存储层:OSS 服务端加密(SSE-KMS)
+
+### 8.3 访问日志
+
+- Nginx 访问日志记录
+- API 网关请求日志
+
+---
+
+## 9. 成本预估
+
+### 9.1 服务器成本
+
+| 资源 | 规格 | 月费用 |
+|------|------|--------|
+| ECS | 2核2G | ~80元 |
+| 数据盘 | 20GB SSD | ~10元 |
+| **合计** | - | **~90元/月** |
+
+### 9.2 OSS 存储成本
+
+假设日日志量:1GB
+
+| 存储类型 | 容量 | 月费用 |
+|----------|------|--------|
+| 标准存储 | 30GB | ~3.6元 |
+| 低频存储 | 60GB | ~4.2元 |
+| 归档存储 | 275GB | ~3.85元 |
+| **合计** | - | **~11.65元/月** |
+
+### 9.3 总成本
+
+**~100元/月**(含服务器和存储)
+
+---
+
+## 10. 扩展规划
+
+### 10.1 短期(3个月内)
+
+- 完成核心功能开发
+- 集成 Sentry 协议
+- 部署上线
+
+### 10.2 中期(6个月内)
+
+- 增加日志采样功能
+- 优化查询性能
+- 增加告警功能
+
+### 10.3 长期(1年内)
+
+- 支持更多日志来源
+- 增加分布式追踪
+- 考虑升级到更强配置
+
+---
+
+## 附录:组件版本建议
+
+| 组件 | 推荐版本 | 备注 |
+|------|---------|------|
+| Node.js | 20.x LTS | API网关运行环境 |
+| Express | 4.x | API框架 |
+| Loki | 2.9.x | 日志存储 |
+| Grafana | 10.x | 可视化 |
+| Docker | 24.x | 容器化部署 |
+
+---
+
+**文档版本**: v1.0
+**创建日期**: 2026-06-16
+**适用场景**: 2核2G ECS + 阿里云OSS 资源受限环境
\ No newline at end of file
diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md
new file mode 100644
index 0000000..766c182
--- /dev/null
+++ b/docs/INTEGRATION.md
@@ -0,0 +1,658 @@
+# 业务接入指南
+
+本文档说明如何将前端/后端业务接入 light-sentry 日志分析系统。
+
+---
+
+## 目录
+
+1. [系统架构](#1-系统架构)
+2. [接入方式概览](#2-接入方式概览)
+3. [前端接入(Browser SDK)](#3-前端接入browser-sdk)
+4. [后端接入(Node.js SDK)](#4-后端接入nodejs-sdk)
+5. [Python 接入](#5-python-接入)
+6. [容器日志接入](#6-容器日志接入)
+7. [项目与容器关联](#7-项目与容器关联)
+8. [一键部署业务机](#8-一键部署业务机)
+9. [常见问题](#9-常见问题)
+
+---
+
+## 1. 系统架构
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ log.starseekai.cn │
+│ │
+│ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │
+│ │ /manage/ │ │ /grafana/ │ │ /dozzle/ │ │
+│ │ 项目管理 │ │ 仪表盘 │ │ 容器日志查看 │ │
+│ └────┬─────┘ └─────┬──────┘ └──────┬───────┘ │
+│ │ │ │ │
+│ ┌────▼────────────────▼────────────────▼───────┐ │
+│ │ Nginx 反向代理 │ │
+│ │ /api/* → API 服务 (9000) │ │
+│ │ /loki/* → Loki (3100) │ │
+│ └────────────────┬───────────────────────────────┘ │
+│ │ │
+│ ┌────────────────▼───────────────────────┐ │
+│ │ Light-Sentry API 服务 │ │
+│ │ - Sentry SDK 兼容网关 │ │
+│ │ - 项目管理 API │ │
+│ │ - 一键部署 Agent │ │
+│ └────────┬──────────────┬─────────────────┘ │
+│ │ │ │
+│ ┌────────▼───┐ ┌───────▼──────┐ ┌─────────────┐ │
+│ │ Loki │ │ Promtail │ │ Dozzle │ │
+│ │ 日志存储 │ │ 中心日志采集 │ │ 容器日志查看 │ │
+│ └─────────────┘ └──────────────┘ └─────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+
+业务机部署:
+┌─────────────────────────────────────────────────┐
+│ 业务 ECS 服务器 │
+│ │
+│ ┌──────────────┐ ┌──────────────────┐ │
+│ │ Promtail │───►│ 中心 Loki │ │
+│ │ (采集容器日志) │ │ log.starseekai.cn│ │
+│ └──────────────┘ └──────────────────┘ │
+│ │
+│ ┌──────────────┐ │
+│ │ Dozzle │───► 远程连接中心 Dozzle Agent │
+│ │ (本地容器日志) │ │
+│ └──────────────┘ │
+└─────────────────────────────────────────────────┘
+```
+
+### 服务入口
+
+| 服务 | 地址 | 说明 |
+|------|------|------|
+| 项目管理 | `https://log.starseekai.cn/manage/` | 可视化管理项目、容器关联、一键部署 |
+| Grafana 仪表盘 | `https://log.starseekai.cn/grafana/` | 查询 Sentry 日志、容器日志、链路追踪 |
+| Dozzle 容器日志 | `https://log.starseekai.cn/dozzle/` | 实时查看业务机 Docker 容器日志 |
+| Loki 查询 API | `https://log.starseekai.cn/loki/loki/api/v1/` | Loki 原生查询 API |
+| SDK 上报地址 | `https://log.starseekai.cn/api/{projectId}/store/` | Sentry SDK 上报端点 |
+
+---
+
+## 2. 接入方式概览
+
+light-sentry 完全兼容 Sentry SDK 协议,只需将 SDK 的 **DSN(Data Source Name)** 指向本系统即可。
+
+### 接入矩阵
+
+| 场景 | 推荐 SDK | 支持协议 |
+|------|---------|---------|
+| 前端浏览器 | Sentry Browser SDK | Store API + Envelope |
+| Node.js 后端服务 | Sentry Node SDK | Store API + Envelope |
+| Python 后端服务 | Sentry Python SDK | Store API + Envelope |
+| Java Spring Boot | Sentry Java SDK | Store API + Envelope |
+| Go 服务 | Sentry Go SDK | Store API + Envelope |
+
+### DSN 格式
+
+Sentry SDK 要求 DSN 格式为:
+
+```
+https://{public_key}@{host}/{projectId}
+```
+
+| 参数 | 说明 | 示例 |
+|------|------|------|
+| `public_key` | 认证公钥(可自定义,建议用项目名) | `order-frontend`、`backend-api` |
+| `host` | 本系统的入口地址 | `log.starseekai.cn` |
+| `projectId` | **必须是纯数字** | `1001`、`1002`、`1003` |
+
+**重要:** Sentry SDK 要求 `projectId` 必须是纯数字,不支持字母或字符串 ID。
+
+**示例 DSN:**
+
+```bash
+# 前端项目
+https://order-frontend@log.starseekai.cn/1001
+
+# 后端项目
+https://order-backend@log.starseekai.cn/1002
+
+# Python 服务
+https://payment-service@log.starseekai.cn/1003
+```
+
+### 接入流程
+
+```
+1. 在项目管理页面 (https://log.starseekai.cn/manage/) 创建项目
+2. 填写纯数字项目 ID(如 1001)
+3. 填写 Public Key(可自定义,或留空自动生成)
+4. 获取生成的 DSN
+5. 在业务代码中配置 SDK
+6. 容器日志:通过一键部署或手动配置 Promtail
+7. 在 Grafana 仪表盘查看日志
+```
+
+### 接入建议:前后端分开
+
+**建议将前端和后端作为两个独立项目接入:**
+
+```
+业务系统
+├── 前端项目 → projectId: 1001, publicKey: myapp-frontend
+└── 后端项目 → projectId: 1002, publicKey: myapp-backend
+```
+
+**分开的好处:**
+
+| 维度 | 说明 |
+|------|------|
+| **日志区分** | 前后端错误分开,一目了然 |
+| **采样率** | 前端可设置更高采样(用户行为重要),后端可更低(日志量大) |
+| **告警规则** | 前端错误率 > 1% 告警,后端 > 5% 告警 |
+| **链路追踪** | 通过 trace_id 仍能串联前后端日志 |
+
+---
+
+## 3. 前端接入(Browser SDK)
+
+### 安装
+
+```bash
+npm install @sentry/browser
+# 或
+yarn add @sentry/browser
+# 或
+pnpm add @sentry/browser
+```
+
+### 基础接入(HTML 单页)
+
+```html
+
+
+```
+
+### React / Vue / Next.js 接入
+
+**React(Create React App / Next.js):**
+
+```javascript
+import * as Sentry from '@sentry/browser';
+import { BrowserTracing } from '@sentry/tracing';
+
+Sentry.init({
+ dsn: 'https://frontend@log.starseekai.cn/1001',
+ integrations: [new BrowserTracing()],
+ tracesSampleRate: 0.1,
+ environment: process.env.NODE_ENV,
+ release: process.env.npm_package_version,
+});
+```
+
+**Vue 3:**
+
+```javascript
+import { createApp } from 'vue';
+import * as Sentry from '@sentry/browser';
+import { VueIntegration } from '@sentry/integrations';
+
+Sentry.init({
+ dsn: 'https://frontend@log.starseekai.cn/1001',
+ integrations: [new VueIntegration({ app })],
+ tracesSampleRate: 0.1,
+ environment: process.env.NODE_ENV,
+});
+```
+
+### 手动上报错误
+
+```javascript
+// 手动捕获并上报
+try {
+ // 业务代码
+ JSON.parse('not valid json');
+} catch (err) {
+ Sentry.captureException(err, {
+ // 自定义额外数据
+ extra: {
+ userId: currentUser.id,
+ action: 'checkout',
+ },
+ });
+}
+
+// 上报自定义消息
+Sentry.captureMessage('用户注册成功', {
+ level: 'info',
+ extra: { userId: 12345, plan: 'pro' },
+});
+```
+
+### 添加用户上下文
+
+```javascript
+// 登录时设置
+Sentry.setUser({
+ id: 'user_12345',
+ email: 'user@example.com',
+ username: 'john_doe',
+ ip_address: '{{auto}}', // 自动采集 IP
+});
+
+// 登出时清除
+Sentry.setUser(null);
+```
+
+---
+
+## 4. 后端接入(Node.js SDK)
+
+### 安装
+
+```bash
+npm install @sentry/node
+# 或
+yarn add @sentry/node
+```
+
+### Express 接入
+
+```javascript
+const express = require('express');
+const Sentry = require('@sentry/node');
+const { ExpressIntegration } = require('@sentry/integrations');
+
+const app = express();
+
+// Sentry 必须在其他中间件之前初始化
+Sentry.init({
+ dsn: 'https://backend@log.starseekai.cn/1002',
+ integrations: [new ExpressIntegration({ app })],
+ tracesSampleRate: 0.1,
+ environment: process.env.NODE_ENV,
+ release: process.env.npm_package_version,
+});
+
+// 请求处理中间件(必须)
+app.use(Sentry.Handlers.requestHandler());
+
+// 你的业务路由
+app.get('/api/order', (req, res) => {
+ // 业务代码
+ res.json({ orderId: 'ORD_12345' });
+});
+
+// 错误处理中间件(必须)
+app.use(Sentry.Handlers.errorHandler());
+
+app.listen(3000);
+```
+
+### 自动捕获未处理错误和 Promise rejections
+
+```javascript
+// 在 Sentry.init() 之后添加
+
+// 捕获未处理的 Promise rejection
+process.on('unhandledRejection', (reason, promise) => {
+ Sentry.captureException(reason);
+});
+
+// 捕获未处理的同步异常
+process.on('uncaughtException', (err) => {
+ Sentry.captureException(err);
+ process.exit(1);
+});
+```
+
+### 手动上报错误
+
+```javascript
+const Sentry = require('@sentry/node');
+
+async function createOrder(req, res) {
+ try {
+ const order = await db.orders.create(req.body);
+ Sentry.addBreadcrumb({
+ category: 'db',
+ message: 'Order created',
+ data: { orderId: order.id },
+ });
+ res.json(order);
+ } catch (err) {
+ Sentry.captureException(err, {
+ // 附加请求上下文
+ contexts: {
+ request: {
+ method: req.method,
+ url: req.url,
+ headers: req.headers,
+ },
+ },
+ });
+ throw err; // 仍需抛出,让 Express 错误中间件处理
+ }
+}
+```
+
+---
+
+## 5. Python 接入
+
+### 安装
+
+```bash
+pip install sentry-sdk
+```
+
+### Django / Flask 接入
+
+**Django:**
+
+```python
+import sentry_sdk
+from sentry_sdk.integrations.django import DjangoIntegration
+
+sentry_sdk.init(
+ dsn='https://python-service@log.starseekai.cn/1003',
+ integrations=[DjangoIntegration()],
+ traces_sample_rate=0.1,
+ environment='production',
+ release='v1.2.3',
+)
+```
+
+**Flask:**
+
+```python
+import sentry_sdk
+from sentry_sdk.integrations.flask import FlaskIntegration
+
+sentry_sdk.init(
+ dsn='https://python-service@log.starseekai.cn/1003',
+ integrations=[FlaskIntegration()],
+ traces_sample_rate=0.1,
+ environment='production',
+)
+```
+
+### 手动上报
+
+```python
+from sentry_sdk import capture_exception, capture_message
+
+try:
+ 1 / 0
+except Exception as e:
+ capture_exception(e)
+
+capture_message('用户登录成功', level='info', extra={'user_id': 12345})
+```
+
+---
+
+## 6. 容器日志接入
+
+容器日志通过 Promtail 采集,存储到 Loki,可在 Grafana 和 Dozzle 中查看。
+
+### 6.1 中心 Promtail(采集本机日志)
+
+适用于日志分析系统所在的 ECS 服务器。
+
+编辑 `promtail/promtail-config.yml`,修改容器白名单:
+
+```yaml
+scrape_configs:
+ - docker_sd_configs:
+ - host: unix:///var/run/docker.sock
+ refresh_interval: 5s
+ relabel_configs:
+ # 白名单:只收集以下前缀的容器(多个用 | 分隔)
+ - source_labels: ['__meta_docker_container_name']
+ regex: '/(light-sentry-.+|myapp-.+|nginx)'
+ action: keep
+```
+
+### 6.2 业务机 Promtail(一键部署)
+
+通过项目管理页面的「一键部署」功能,自动将 Promtail Agent 部署到业务 ECS 服务器。
+
+**部署步骤:**
+
+1. 打开 `https://log.starseekai.cn/manage/`
+2. 编辑项目,填写「关联容器名称」或「容器过滤正则」
+3. 点击「一键部署」,填写业务机 SSH 信息
+4. 系统自动完成:SSH 连接 → 生成配置 → 部署 Promtail → 重启 Dozzle Agent
+
+### 6.3 Dozzle 容器过滤
+
+Dozzle 默认只显示 `light-sentry-*` 前缀的容器。修改 `.env` 可调整:
+
+```bash
+# 格式:key=value,支持正则,多个用逗号分隔
+DOZZLE_FILTER=name=light-sentry-.+|myapp-.+|nginx
+```
+
+### 6.4 Grafana 中查询容器日志
+
+Promtail 采集的日志带以下标签:
+
+| 标签 | 说明 |
+|------|------|
+| `host` | 主机名 |
+| `source` | `container` 标识为容器日志 |
+| `container_name` | Docker 容器名称 |
+| `container_image` | Docker 镜像名称 |
+| `composed_project` | Docker Compose 项目名 |
+| `docker_service` | Docker Compose 服务名 |
+
+**查询示例:**
+
+```logql
+# 按容器名查询
+{container_name="myapp-web"}
+
+# 按镜像名查询
+{container_image=~"nginx.*"}
+
+# 组合 Sentry 项目 + 容器日志
+{container_name=~"myapp-.*", host="prod-ecs"}
+
+# 查看该容器最近 1 小时的日志
+{container_name="myapp-web"} |= "ERROR" | __error__!="JSONErr"
+```
+
+---
+
+## 7. 项目与容器关联
+
+### 7.1 关联步骤
+
+1. 打开项目管理页面 `https://log.starseekai.cn/manage/`
+2. 编辑项目,填写容器信息:
+ - **关联容器名称**:精确匹配容器名,多个用逗号分隔
+ - **容器过滤正则**(可选):支持正则表达式,优先级更高
+
+| 关联方式 | 示例 | 说明 |
+|---------|------|------|
+| 单容器 | `myapp-web` | 只采集指定容器 |
+| 多容器 | `myapp-web, myapp-api` | 逗号分隔 |
+| 正则匹配 | `myapp-.*` | 匹配所有 myapp- 开头的容器 |
+
+### 7.2 接入声明
+
+项目管理页面提供一键复制功能,包含:
+
+- SDK 接入代码片段
+- 完整的 DSN 地址
+- Grafana 仪表盘跳转链接
+- Dozzle 容器日志跳转链接
+
+### 7.3 项目管理 API
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| `GET` | `/api/projects/` | 列出所有项目 |
+| `GET` | `/api/projects/:id` | 获取单个项目详情 |
+| `POST` | `/api/projects/` | 创建新项目 |
+| `PUT` | `/api/projects/:id` | 更新项目信息 |
+| `DELETE` | `/api/projects/:id` | 删除项目 |
+
+**创建项目示例:**
+
+```bash
+curl -X POST https://log.starseekai.cn/api/projects/ \
+ -H "Content-Type: application/json" \
+ -d '{
+ "id": "order-service",
+ "name": "订单服务",
+ "platform": "node",
+ "environment": "production",
+ "description": "处理订单创建和支付",
+ "containerName": "order-service-web, order-service-worker"
+ }'
+```
+
+---
+
+## 8. 一键部署业务机
+
+通过项目管理页面的「一键部署」功能,将 Promtail Agent 和 Dozzle Agent 自动部署到业务 ECS 服务器。
+
+### 8.1 部署流程
+
+```
+1. 用户在页面填写业务机 SSH 信息(IP、用户名、密码)
+2. 后端启动异步部署任务
+3. SSH 连接业务机
+4. 创建部署目录、生成 Promtail/Dozzle 配置
+5. SCP 上传配置文件和 docker-compose.yml
+6. 启动容器
+7. 同步 Agent 信息到中心 Dozzle
+8. 实时 SSE 推送部署日志到前端
+```
+
+### 8.2 部署配置说明
+
+**Loki Push 地址**:由项目管理页面的「中心 Loki 地址」配置,默认为 `https://log.starseekai.cn/loki/loki/api/v1/push`
+
+**部署目录**:默认 `/opt/light-sentry-agent`,可在部署时自定义
+
+**容器过滤**:根据项目关联的容器名自动生成 Promtail 过滤正则
+
+### 8.3 部署目录结构
+
+```
+/opt/light-sentry-agent/
+├── docker-compose.yml # Dozzle Agent
+├── promtail/
+│ └── promtail-config.yml # Promtail 配置
+└── .env # 环境变量(DOZZLE_REMOTE_AGENT)
+```
+
+### 8.4 查看部署历史
+
+部署历史可在项目管理页面查看,包含:
+
+- 部署时间、目标主机
+- 部署状态(成功/失败)
+- 实时部署日志
+- 容器过滤配置
+
+---
+
+## 9. 常见问题
+
+### Q: SDK 上报后看不到日志
+
+**排查步骤:**
+
+1. 检查浏览器控制台是否有 Sentry 报错
+2. 确认 DSN 是否正确:`https://{publicKey}@log.starseekai.cn/{projectId}`
+3. 确认 `projectId` 是否是纯数字(如 `1001`)
+4. 在 Grafana Explore 中直接查询原始日志:
+
+```logql
+{platform="javascript"}
+```
+
+### Q: 容器日志没有采集到
+
+**排查步骤:**
+
+1. 确认 Promtail 容器是否在运行:`docker ps | grep promtail`
+2. 检查 Promtail 日志:`docker logs light-sentry-promtail`
+3. 确认容器名是否在白名单中
+4. 检查 Loki 是否收到数据:`curl http://localhost:3100/loki/api/v1/label/container_name/values`
+
+### Q: 一键部署失败
+
+**常见原因:**
+
+1. SSH 密码错误或 IP 无法连接
+2. 目标目录权限不足(确保 SSH 用户有 `/opt` 写权限)
+3. Docker 未安装或未启动
+
+**解决方案:**
+
+- 手动检查 SSH 连接:`ssh user@host`
+- 检查目标目录权限:`ls -la /opt/`
+- 查看部署日志中的详细错误信息
+
+### Q: 生产环境建议采样率是多少?
+
+| 环境 | tracesSampleRate | 说明 |
+|------|-----------------|------|
+| 开发/测试 | `1.0` | 全量采集,方便调试 |
+| 预发/灰度 | `0.5` | 采集一半 |
+| 生产 | `0.05 ~ 0.2` | 5%~20%,平衡性能和数据量 |
+
+### Q: 如何在 Grafana 中切换项目查看?
+
+在 Grafana 仪表盘顶部有 **项目** 下拉框,选择后可按项目过滤日志。仪表盘地址格式:
+
+```
+https://log.starseekai.cn/grafana/d/light-sentry-logs?var-project=1
+```
+
+其中 `var-project` 参数值为项目 ID。
+
+### Q: 日志量会不会太大?
+
+假设:
+- 每小时 10000 次 API 请求
+- tracesSampleRate = 0.1(10%)
+- 每条 trace 上报 3~5 条日志
+- 每条日志约 2KB
+
+```
+月流量 ≈ 10000 × 0.1 × 4 × 2KB × 24h × 30d ≈ 57 MB
+```
+
+非常轻量,2核2G 完全没压力。
+
+### Q: 如何添加自定义标签?
+
+```javascript
+// 设置全局标签(所有事件都会带)
+Sentry.setTag('server', 'production');
+Sentry.setTag('region', 'cn-hangzhou');
+
+// 设置用户级别标签
+Sentry.setContext('order', {
+ orderId: 'ORD_12345',
+ amount: 99.9,
+ currency: 'CNY',
+});
+```
diff --git a/src/core/Client.ts b/src/core/Client.ts
index 28e81ed..0577b39 100644
--- a/src/core/Client.ts
+++ b/src/core/Client.ts
@@ -1,7 +1,8 @@
-import type { LightConfig, LightClient, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb, ErrorEvent } from '../types';
+import type { LightConfig, LightClient, LightPlugin, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb, ErrorEvent } from '../types';
import { parseDSN } from '../utils/dsn';
import { now } from '../utils/helper';
import { computeFingerprint } from '../utils/hash';
+import { parseStackTrace } from '../utils/stacktrace';
import { ConfigManager } from './ConfigManager';
import { EventBus } from './EventBus';
import { EventQueue } from './EventQueue';
@@ -9,7 +10,6 @@ import { Reporter } from './Reporter';
import { PluginManager } from './PluginManager';
class Client implements LightClient {
- config: LightConfig;
dsn: DSNInfo;
private configManager: ConfigManager;
@@ -18,9 +18,16 @@ class Client implements LightClient {
private reporter: Reporter;
private pluginManager: PluginManager;
private breadcrumbs: Breadcrumb[] = [];
- private maxBreadcrumbs = 20;
private enabled: boolean = true;
+ private get maxBreadcrumbs(): number {
+ return this.configManager.get('maxBreadcrumbs') ?? 20;
+ }
+
+ get config(): LightConfig {
+ return this.configManager.getAll();
+ }
+
constructor(config: LightConfig) {
if (!config.dsn) {
throw new Error('DSN is required');
@@ -28,23 +35,22 @@ class Client implements LightClient {
this.dsn = parseDSN(config.dsn);
this.configManager = new ConfigManager(config);
- this.config = this.configManager.getAll();
this.eventBus = new EventBus();
this.reporter = new Reporter(
this.dsn,
- config.maxRetries ?? 3,
- config.retryDelay ?? 1000
+ this.configManager.get('maxRetries') ?? 3,
+ this.configManager.get('retryDelay') ?? 1000
);
this.pluginManager = new PluginManager(this);
this.queue = new EventQueue(
- config.maxQueueSize ?? 100,
- config.flushInterval ?? 5000,
+ this.configManager.get('maxQueueSize') ?? 100,
+ this.configManager.get('flushInterval') ?? 5000,
this.eventBus,
async (events) => this.flushEvents(events),
(events) => this.syncFlushEvents(events)
);
- this.enabled = config.enabled !== false;
+ this.enabled = this.configManager.get('enabled') !== false;
}
init(): void {
@@ -65,28 +71,23 @@ class Client implements LightClient {
if (processedEvents.length === 0) return;
- // 批次内优化:只有第一个事件带完整环境信息
- // 后续事件只带 context_id,服务端根据 ID 关联
let isFirstInBatch = true;
const finalEvents: SentryEvent[] = [];
for (const event of processedEvents) {
- // 标记是否为批次首个事件
const withConfig = this.configManager.applyToEvent(event, isFirstInBatch);
- isFirstInBatch = false; // 后续事件不再带完整 contexts
+ isFirstInBatch = false;
- const finalEvent = this.config.get('beforeSend')
- ? this.config.get('beforeSend')!(withConfig)
- : withConfig;
- if (finalEvent) {
- finalEvents.push(finalEvent);
- }
+ const beforeSend = this.configManager.get('beforeSend');
+ const afterBeforeSend = beforeSend ? beforeSend(withConfig) : withConfig;
+ if (!afterBeforeSend) continue;
+
+ finalEvents.push(afterBeforeSend);
}
if (finalEvents.length === 0) return;
await this.reporter.report(finalEvents);
- // 批次结束后标记,后续批次不再带完整 contexts
this.configManager.markContextReported();
for (const event of finalEvents) {
@@ -106,7 +107,6 @@ class Client implements LightClient {
if (processedEvents.length === 0) return;
- // 批次内优化:只有第一个事件带完整环境信息
let isFirstInBatch = true;
const finalEvents: SentryEvent[] = [];
@@ -114,12 +114,11 @@ class Client implements LightClient {
const withConfig = this.configManager.applyToEvent(event, isFirstInBatch);
isFirstInBatch = false;
- const finalEvent = this.config.get('beforeSend')
- ? this.config.get('beforeSend')!(withConfig)
- : withConfig;
- if (finalEvent) {
- finalEvents.push(finalEvent);
- }
+ const beforeSend = this.configManager.get('beforeSend');
+ const afterBeforeSend = beforeSend ? beforeSend(withConfig) : withConfig;
+ if (!afterBeforeSend) continue;
+
+ finalEvents.push(afterBeforeSend);
}
if (finalEvents.length === 0) return;
@@ -141,7 +140,7 @@ class Client implements LightClient {
}
captureException(error: Error | unknown): void {
- if (!this.enabled || !this.configManager.shouldSample()) return;
+ if (!this.enabled) return;
const errorEvent = this.buildErrorEvent(error);
if (this.configManager.isIgnoredError(errorEvent.message)) return;
@@ -151,7 +150,7 @@ class Client implements LightClient {
}
captureMessage(message: string, level: EventLevel = 'info'): void {
- if (!this.enabled || !this.configManager.shouldSample()) return;
+ if (!this.enabled) return;
const event: ErrorEvent = {
type: 'error',
@@ -167,11 +166,12 @@ class Client implements LightClient {
}
captureEvent(event: Partial & { type: string }): void {
- if (!this.enabled || !this.configManager.shouldSample()) return;
+ if (!this.enabled) return;
const fullEvent = {
timestamp: now(),
level: 'info' as EventLevel,
+ breadcrumbs: [...this.breadcrumbs],
...event,
} as SentryEvent;
@@ -182,7 +182,7 @@ class Client implements LightClient {
const timestamp = now();
if (error instanceof Error) {
- const frames = this.parseStackTrace(error.stack);
+ const frames = parseStackTrace(error.stack, { maxFrames: 5 });
const fingerprint = computeFingerprint(error.name, error.message, frames);
return {
@@ -194,7 +194,7 @@ class Client implements LightClient {
type: error.name,
value: error.message,
stacktrace: {
- frames: frames.slice(0, 5),
+ frames,
},
},
fingerprint,
@@ -210,40 +210,6 @@ class Client implements LightClient {
};
}
- private parseStackTrace(stack?: string): { filename: string; function?: string; lineno?: number; colno?: number; in_app?: boolean }[] {
- if (!stack) return [];
-
- const frames: { filename: string; function?: string; lineno?: number; colno?: number; in_app?: boolean }[] = [];
- const lines = stack.split('\n');
-
- for (const line of lines) {
- const match = line.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);
- if (match) {
- const [, fn, filename, lineno, colno] = match;
- frames.push({
- filename,
- function: fn,
- lineno: parseInt(lineno, 10),
- colno: parseInt(colno, 10),
- in_app: !filename.includes('node_modules'),
- });
- } else {
- const urlMatch = line.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);
- if (urlMatch) {
- const [, filename, lineno, colno] = urlMatch;
- frames.push({
- filename,
- lineno: parseInt(lineno, 10),
- colno: parseInt(colno, 10),
- in_app: !filename.includes('node_modules'),
- });
- }
- }
- }
-
- return frames.reverse();
- }
-
setUser(user: UserInfo | null): void {
this.configManager.setUser(user);
}
@@ -284,10 +250,8 @@ class Client implements LightClient {
this.enabled = true;
}
- use(plugin: unknown): void {
- if (plugin && typeof plugin === 'object' && 'name' in plugin && 'setup' in plugin) {
- this.pluginManager.add(plugin as never);
- }
+ use(plugin: LightPlugin): void {
+ this.pluginManager.add(plugin);
}
destroy(): void {
diff --git a/src/core/ConfigManager.ts b/src/core/ConfigManager.ts
index fd8a9c8..ca76418 100644
--- a/src/core/ConfigManager.ts
+++ b/src/core/ConfigManager.ts
@@ -1,5 +1,6 @@
-import type { LightConfig, SentryEvent, UserInfo } from '../types';
+import type { LightConfig, SentryEvent, UserInfo, ContextLevelMap, ErrorEvent, StackFrame, Breadcrumb, EventLevel } from '../types';
import { getContexts, getContextId, getPageUrl, getReferrer } from '../utils/env';
+import { matchPatterns } from '../utils/helper';
const DEFAULT_CONFIG: Partial = {
enabled: true,
@@ -57,7 +58,7 @@ export class ConfigManager {
if (!this.config.tags) {
this.config.tags = {};
}
- this.config.tags[key] = value;
+ (this.config.tags as Record)[key] = value;
}
setExtra(key: string, value: unknown): void {
@@ -67,21 +68,8 @@ export class ConfigManager {
(this.config.extra as Record)[key] = value;
}
- shouldSample(): boolean {
- const rate = this.config.sampleRate ?? 1;
- if (rate >= 1) return true;
- if (rate <= 0) return false;
- return Math.random() < rate;
- }
-
isIgnoredError(message: string): boolean {
- const patterns = this.config.ignoreErrors || [];
- return patterns.some(pattern => {
- if (typeof pattern === 'string') {
- return message.includes(pattern);
- }
- return pattern.test(message);
- });
+ return matchPatterns(message, this.config.ignoreErrors || []);
}
/**
@@ -101,49 +89,81 @@ export class ConfigManager {
* @param includeFullContext 是否包含完整环境信息(批次内只有首个事件需要)
*/
applyToEvent(event: SentryEvent, includeFullContext: boolean = true): SentryEvent {
- if (this.config.release) {
- event.release = this.config.release;
- }
- if (this.config.environment) {
- event.environment = this.config.environment;
- }
- if (this.config.user) {
- event.user = this.config.user;
- }
- if (this.config.tags) {
- event.tags = { ...this.config.tags, ...(event.tags || {}) };
+ // 使用 Record 来避免联合类型的问题
+ const enriched: Record = { ...event };
+
+ // 处理 tags
+ if (event.tags) {
+ enriched.tags = { ...event.tags };
}
- if (!event.request) {
- event.request = {
+ // 处理 request
+ if (event.request) {
+ enriched.request = { ...event.request };
+ }
+
+ // 处理 exception(仅 error 事件有)
+ if ('exception' in event && event.exception) {
+ const exc = event.exception;
+ enriched.exception = {
+ ...exc,
+ stacktrace: exc.stacktrace ? {
+ ...exc.stacktrace,
+ frames: exc.stacktrace.frames ? exc.stacktrace.frames.map((f: StackFrame) => ({ ...f })) : undefined,
+ } : undefined,
+ };
+ }
+
+ // 处理 breadcrumbs
+ if (event.breadcrumbs) {
+ enriched.breadcrumbs = event.breadcrumbs.map((b: Breadcrumb) => ({ ...b }));
+ }
+
+ // 处理 user
+ if (event.user) {
+ enriched.user = { ...event.user };
+ }
+
+ // 处理 contexts
+ if (event.contexts) {
+ enriched.contexts = { ...event.contexts };
+ }
+
+ if (this.config.release) {
+ enriched.release = this.config.release;
+ }
+ if (this.config.environment) {
+ enriched.environment = this.config.environment;
+ }
+ if (this.config.user) {
+ enriched.user = this.config.user;
+ }
+ if (this.config.tags) {
+ enriched.tags = { ...this.config.tags, ...(event.tags as Record || {}) };
+ }
+
+ if (!enriched.request) {
+ enriched.request = {
url: getPageUrl(),
referrer: getReferrer(),
};
- } else if (!event.request.url) {
- event.request.url = getPageUrl();
+ } else if (!(enriched.request as Record).url) {
+ (enriched.request as Record).url = getPageUrl();
}
- // context_id 总是带上,用于服务端关联环境信息
if (!this.contextId) {
this.contextId = getContextId();
}
- event.context_id = this.contextId;
+ enriched.context_id = this.contextId;
- // 应用上下文分层策略:按事件级别裁剪堆栈和 breadcrumbs
- this.applyContextLevel(event);
+ const result = this.applyContextLevel(enriched);
- // 只有需要完整环境信息时才附加 contexts
- // 批次内:首个事件带完整 contexts,后续事件只带 context_id
- // 跨批次:首次上报带完整 contexts,后续批次只带 context_id
if (includeFullContext && !this.contextReported) {
if (!this.contexts) {
this.contexts = getContexts();
}
- // 完整环境信息(已编码:browser.name -> c, os.name -> m 等)
- event.contexts = this.contexts;
- // 额外附加 env 字段,方便服务端识别编码格式
- // env 字段使用短字段名:b=浏览器, bv=浏览器版本, os=系统, osv=系统版本
- (event as Record).env = {
+ (result as Record).contexts = this.contexts;
+ (result as Record).env = {
b: this.contexts.browser?.name,
bv: this.contexts.browser?.version,
os: this.contexts.os?.name,
@@ -151,33 +171,55 @@ export class ConfigManager {
};
}
- return event;
+ return result as unknown as SentryEvent;
}
/**
* 应用上下文分层策略:根据事件级别裁剪堆栈和 breadcrumbs
+ * 注意:此方法不修改原始 event 对象,返回裁剪后的副本
*/
- private applyContextLevel(event: SentryEvent): void {
- const contextLevel = this.config.contextLevel as Record;
- if (!contextLevel) return;
+ private applyContextLevel(event: Record): Record {
+ const contextLevel = this.config.contextLevel as ContextLevelMap | undefined;
+ if (!contextLevel) return event;
- const level = event.level || 'error';
+ const level = (event.level || 'error') as EventLevel;
const levelConfig = contextLevel[level] || contextLevel['error'];
- // 裁剪堆栈帧
- if (event.exception?.stacktrace?.frames && levelConfig.maxStackFrames > 0) {
- event.exception.stacktrace.frames = event.exception.stacktrace.frames.slice(0, levelConfig.maxStackFrames);
- } else if (levelConfig.maxStackFrames === 0 && event.exception?.stacktrace?.frames) {
- // info/debug 级别不需要堆栈
- delete event.exception.stacktrace;
+ // 创建浅拷贝用于裁剪
+ const enriched: Record = { ...event };
+
+ // 裁剪堆栈帧(仅 error 事件有 exception)
+ if ('exception' in event && event.exception && typeof event.exception === 'object') {
+ const exc = event.exception as Record;
+ if (exc.stacktrace && typeof exc.stacktrace === 'object') {
+ const st = exc.stacktrace as Record;
+ if (Array.isArray(st.frames)) {
+ if (levelConfig.maxStackFrames === 0) {
+ // info/debug 级别不需要堆栈
+ delete enriched.exception;
+ } else {
+ enriched.exception = {
+ ...exc,
+ stacktrace: {
+ ...st,
+ frames: (st.frames as StackFrame[]).slice(0, levelConfig.maxStackFrames),
+ },
+ };
+ }
+ }
+ }
}
// 裁剪 breadcrumbs
- if (event.breadcrumbs && levelConfig.maxBreadcrumbs > 0) {
- event.breadcrumbs = event.breadcrumbs.slice(-levelConfig.maxBreadcrumbs);
- } else if (levelConfig.maxBreadcrumbs === 0) {
- // info/debug 级别不需要 breadcrumbs
- delete event.breadcrumbs;
+ if (event.breadcrumbs && Array.isArray(event.breadcrumbs)) {
+ if (levelConfig.maxBreadcrumbs === 0) {
+ // info/debug 级别不需要 breadcrumbs
+ delete enriched.breadcrumbs;
+ } else {
+ enriched.breadcrumbs = (event.breadcrumbs as Breadcrumb[]).slice(-levelConfig.maxBreadcrumbs);
+ }
}
+
+ return enriched;
}
}
diff --git a/src/core/EventQueue.ts b/src/core/EventQueue.ts
index 5d767f9..d12f169 100644
--- a/src/core/EventQueue.ts
+++ b/src/core/EventQueue.ts
@@ -17,6 +17,10 @@ export class EventQueue {
private flushCallback: (events: SentryEvent[]) => Promise;
private syncFlushCallback: (events: SentryEvent[]) => void;
private lastFlushTime: number = 0;
+ private isFlushing: boolean = false;
+ private onVisibilityChange?: () => void;
+ private onBeforeUnload?: () => void;
+ private onPagehide?: () => void;
private dedupeMap: Map = new Map();
private pendingCounts: Map = new Map();
@@ -53,26 +57,28 @@ export class EventQueue {
}
private setupVisibilityListener(): void {
+ this.onVisibilityChange = () => {
+ if (document.hidden && (this.queue.length > 0 || this.pendingCounts.size > 0)) {
+ this.flushSync();
+ }
+ };
if (typeof document !== 'undefined') {
- document.addEventListener('visibilitychange', () => {
- if (document.hidden && (this.queue.length > 0 || this.pendingCounts.size > 0)) {
- this.flushSync();
- }
- });
+ document.addEventListener('visibilitychange', this.onVisibilityChange);
}
+ this.onBeforeUnload = () => {
+ if (this.queue.length > 0 || this.pendingCounts.size > 0) {
+ this.flushSync();
+ }
+ };
+ this.onPagehide = () => {
+ if (this.queue.length > 0 || this.pendingCounts.size > 0) {
+ this.flushSync();
+ }
+ };
if (typeof window !== 'undefined') {
- window.addEventListener('beforeunload', () => {
- if (this.queue.length > 0 || this.pendingCounts.size > 0) {
- this.flushSync();
- }
- });
-
- window.addEventListener('pagehide', () => {
- if (this.queue.length > 0 || this.pendingCounts.size > 0) {
- this.flushSync();
- }
- });
+ window.addEventListener('beforeunload', this.onBeforeUnload);
+ window.addEventListener('pagehide', this.onPagehide);
}
}
@@ -91,6 +97,11 @@ export class EventQueue {
window.shift();
}
+ if (window.length === 0) {
+ delete this.errorRateWindow[fingerprint];
+ return false;
+ }
+
if (window.length > 10) {
if (!this.paused) {
console.warn('[LightSDK] Infinite loop detected, pausing SDK for 60s', { fingerprint, count: window.length });
@@ -115,6 +126,8 @@ export class EventQueue {
enqueue(event: SentryEvent): void {
if (this.paused) return;
+ this.cleanupExpiredDedupeEntries();
+
const fingerprint = this.getDedupeKey(event);
if (fingerprint) {
@@ -148,10 +161,6 @@ export class EventQueue {
}
}
- if (this.queue.length >= this.maxSize) {
- this.flush();
- }
-
this.queue.push(event);
this.eventBus.emit('event', event);
@@ -160,11 +169,24 @@ export class EventQueue {
}
}
+ private cleanupExpiredDedupeEntries(): void {
+ // 每 100 条或每 10 次清理一次,避免过于频繁
+ if (this.dedupeMap.size < 100 && Math.random() > 0.1) return;
+
+ const currentTime = now();
+ const expiryTime = 60000;
+
+ for (const [key, entry] of this.dedupeMap) {
+ if (currentTime - entry.lastTime > expiryTime) {
+ this.dedupeMap.delete(key);
+ }
+ }
+ }
+
private buildCountEvents(): SentryEvent[] {
const countEvents: SentryEvent[] = [];
for (const [fingerprint, data] of this.pendingCounts.entries()) {
- // 构建聚合计数事件
const event: CountEvent = {
type: 'count',
fingerprint,
@@ -177,8 +199,6 @@ export class EventQueue {
countEvents.push(event);
}
- // 清空已上报的聚合计数
- this.pendingCounts.clear();
return countEvents;
}
@@ -192,6 +212,9 @@ export class EventQueue {
async flush(): Promise {
if (this.queue.length === 0 && this.pendingCounts.size === 0) return;
+ if (this.isFlushing) return;
+
+ this.isFlushing = true;
const events = this.queue.splice(0, this.queue.length);
const countEvents = this.buildCountEvents();
@@ -202,10 +225,13 @@ export class EventQueue {
try {
await this.flushCallback(allEvents);
+ this.pendingCounts.clear();
this.eventBus.emit('reported', allEvents);
} catch (e) {
events.forEach(evt => this.queue.unshift(evt));
throw e;
+ } finally {
+ this.isFlushing = false;
}
}
@@ -213,6 +239,7 @@ export class EventQueue {
if (this.queue.length === 0 && this.pendingCounts.size === 0) return;
const events = this.queue.splice(0, this.queue.length);
+ const pendingCountsSnapshot = new Map(this.pendingCounts);
const countEvents = this.buildCountEvents();
const allEvents = [...events, ...countEvents];
@@ -221,9 +248,16 @@ export class EventQueue {
try {
this.syncFlushCallback(allEvents);
+ this.pendingCounts.clear();
this.eventBus.emit('reported', allEvents);
} catch (e) {
console.error('[LightSDK] Sync flush failed', e);
+ events.forEach(evt => this.queue.unshift(evt));
+ for (const [key, value] of pendingCountsSnapshot) {
+ if (!this.pendingCounts.has(key)) {
+ this.pendingCounts.set(key, value);
+ }
+ }
}
}
@@ -240,6 +274,17 @@ export class EventQueue {
clearTimeout(this.pauseTimer);
this.pauseTimer = null;
}
+ if (typeof document !== 'undefined' && this.onVisibilityChange) {
+ document.removeEventListener('visibilitychange', this.onVisibilityChange);
+ }
+ if (typeof window !== 'undefined') {
+ if (this.onBeforeUnload) {
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+ }
+ if (this.onPagehide) {
+ window.removeEventListener('pagehide', this.onPagehide);
+ }
+ }
this.dedupeMap.clear();
this.pendingCounts.clear();
this.errorRateWindow = {};
diff --git a/src/core/Reporter.ts b/src/core/Reporter.ts
index 711311f..d99e835 100644
--- a/src/core/Reporter.ts
+++ b/src/core/Reporter.ts
@@ -1,6 +1,6 @@
import type { DSNInfo, SentryEvent, ErrorEvent } from '../types';
import { getEnvelopeUrl } from '../utils/dsn';
-import { now } from '../utils/helper';
+import { now, generateEventId } from '../utils/helper';
import { encodeFields, FIELD_ENCODE_MAP } from '../utils/field-encoder';
export class Reporter {
@@ -34,7 +34,8 @@ export class Reporter {
if (retries > this.maxRetries) {
throw e;
}
- await this.delay(this.retryDelay * Math.pow(2, retries - 1));
+ const backoff = Math.min(this.retryDelay * Math.pow(2, retries - 1), 5000);
+ await this.delay(backoff);
}
}
}
@@ -68,7 +69,7 @@ export class Reporter {
// 构建 header(包含公共元数据)
const headerObj: Record = {
- event_id: this.generateEventId(),
+ event_id: generateEventId(),
sent_at: new Date().toISOString(),
meta: sharedMeta,
};
@@ -87,16 +88,16 @@ export class Reporter {
for (const event of eventsWithRelativeTs) {
// 移除已共享的字段,减少重复
- let strippedEvent = this.stripSharedFields(event, sharedMeta);
+ let strippedEvent: unknown = this.stripSharedFields(event as unknown as SentryEvent, sharedMeta);
// 短字段编码(如果启用)
if (this.useShortFields) {
- strippedEvent = encodeFields(strippedEvent as Record) as SentryEvent;
+ strippedEvent = encodeFields(strippedEvent as Record);
}
const itemPayload = JSON.stringify(strippedEvent);
const itemHeader = JSON.stringify({
- type: this.getEnvelopeType(event),
+ type: this.getEnvelopeType(event as unknown as SentryEvent),
length: itemPayload.length,
});
items.push(itemHeader, itemPayload);
@@ -114,10 +115,10 @@ export class Reporter {
*
* 这样可以将时间戳从 13 位数字减少到 2-4 位数字
*/
- private applyRelativeTimestamps(events: SentryEvent[], headerObj: Record): SentryEvent[] {
+ private applyRelativeTimestamps(events: SentryEvent[], headerObj: Record): (SentryEvent | Record)[] {
if (events.length <= 1) return events;
- const result: SentryEvent[] = [];
+ const result: (SentryEvent | Record)[] = [];
const baseTimestamp = events[0].timestamp ? new Date(events[0].timestamp).getTime() : Date.now();
// 在 header 中标记使用相对时间戳
@@ -125,11 +126,11 @@ export class Reporter {
headerObj._bt = baseTimestamp; // base timestamp
for (let i = 0; i < events.length; i++) {
- const event = { ...events[i] } as Record;
+ const event: Record = { ...events[i] };
if (i === 0) {
// 第一个事件保持完整时间戳
- result.push(event as SentryEvent);
+ result.push(event as unknown as SentryEvent);
} else {
// 后续事件用差值
const eventTs = event.timestamp ? new Date(event.timestamp as string | number).getTime() : Date.now();
@@ -146,7 +147,7 @@ export class Reporter {
event._dsts = startDelta; // delta start timestamp
}
- result.push(event as SentryEvent);
+ result.push(event);
}
}
@@ -189,7 +190,7 @@ export class Reporter {
}
// 提取 env(编码后的环境信息)
- const firstEnv = (firstEvent as Record).env;
+ const firstEnv = (firstEvent as unknown as Record).env;
if (firstEnv) {
meta.env = firstEnv;
}
@@ -200,8 +201,8 @@ export class Reporter {
/**
* 移除已共享的字段,减少重复
*/
- private stripSharedFields(event: SentryEvent, meta: Record): SentryEvent {
- const stripped = { ...event };
+ private stripSharedFields(event: SentryEvent, meta: Record): Record {
+ const stripped: Record = { ...event };
// 移除已共享的字段
if (meta.release && stripped.release === meta.release) {
@@ -214,7 +215,7 @@ export class Reporter {
delete stripped.user;
}
if (meta.env) {
- delete (stripped as Record).env;
+ delete stripped.env;
}
return stripped;
@@ -231,12 +232,6 @@ export class Reporter {
}
}
- private generateEventId(): string {
- return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, () => {
- return ((Math.random() * 16) | 0).toString(16);
- });
- }
-
private async send(body: string, isSync: boolean = false): Promise {
const url = getEnvelopeUrl(this.dsn);
@@ -252,6 +247,8 @@ export class Reporter {
if (typeof fetch === 'function') {
try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, {
method: 'POST',
body,
@@ -259,7 +256,9 @@ export class Reporter {
'Content-Type': 'application/x-sentry-envelope',
},
keepalive: true,
+ signal: controller.signal,
});
+ clearTimeout(timeoutId);
if (response.ok) return;
const err = new Error(`HTTP ${response.status}`) as Error & { status: number };
err.status = response.status;
@@ -274,6 +273,7 @@ export class Reporter {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, !isSync);
xhr.setRequestHeader('Content-Type', 'application/x-sentry-envelope');
+ xhr.timeout = 10000;
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
@@ -284,6 +284,7 @@ export class Reporter {
}
};
xhr.onerror = () => reject(new Error('Network error'));
+ xhr.ontimeout = () => reject(new Error('Request timeout'));
xhr.send(body);
});
}
@@ -323,7 +324,8 @@ export class Reporter {
const url = getEnvelopeUrl(this.dsn);
const img = new Image();
const encoded = encodeURIComponent(btoa(body));
- img.src = url + '&sentry_data=' + encoded.substring(0, 2000);
+ const separator = url.includes('?') ? '&' : '?';
+ img.src = url + separator + 'sentry_data=' + encoded.substring(0, 2000);
return true;
} catch {
return false;
diff --git a/src/index.ts b/src/index.ts
index ab42bf5..34cb28c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,6 +4,7 @@ import PerformancePlugin from './plugins/PerformancePlugin';
import NetworkPlugin from './plugins/NetworkPlugin';
import BehaviorPlugin from './plugins/BehaviorPlugin';
import OfflinePlugin from './plugins/OfflinePlugin';
+import SamplingPlugin from './plugins/SamplingPlugin';
import type { LightConfig, LightClient, SentryEvent, EventLevel, UserInfo, Breadcrumb, LightPlugin, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, DSNInfo } from './types';
let globalClient: LightClient | null = null;
@@ -12,8 +13,9 @@ function init(config: LightConfig): LightClient {
const client = new Client(config);
const defaultPlugins: LightPlugin[] = [
- new ErrorPlugin(),
new OfflinePlugin(),
+ new SamplingPlugin(),
+ new ErrorPlugin(),
];
const pluginNames = config.plugins?.filter(p => typeof p === 'string') as string[] || [];
@@ -29,18 +31,18 @@ function init(config: LightConfig): LightClient {
}
for (const plugin of defaultPlugins) {
- (client as unknown as { use: (p: LightPlugin) => void }).use(plugin);
+ client.use(plugin);
}
if (config.plugins) {
for (const plugin of config.plugins) {
if (typeof plugin === 'object' && plugin !== null && 'name' in plugin && 'setup' in plugin) {
- (client as unknown as { use: (p: LightPlugin) => void }).use(plugin as LightPlugin);
+ client.use(plugin as LightPlugin);
}
}
}
- (client as unknown as { init: () => void }).init();
+ client.init();
globalClient = client;
return client;
@@ -114,6 +116,7 @@ export {
NetworkPlugin,
BehaviorPlugin,
OfflinePlugin,
+ SamplingPlugin,
};
export type {
diff --git a/src/plugins/BehaviorPlugin.ts b/src/plugins/BehaviorPlugin.ts
index 7251be0..86e6696 100644
--- a/src/plugins/BehaviorPlugin.ts
+++ b/src/plugins/BehaviorPlugin.ts
@@ -1,6 +1,6 @@
import type { LightPlugin, LightClient, BehaviorEvent, LightConfig } from '../types';
import { now, isObject } from '../utils/helper';
-import { getPageUrl } from '../utils/env';
+import { getPageUrl, sanitizeUrl } from '../utils/env';
interface BehaviorPluginConfig {
capturePV?: boolean;
@@ -26,7 +26,7 @@ class BehaviorPlugin implements LightPlugin {
captureScroll: true,
clickThrottle: 300,
scrollThrottle: 1000,
- sampleRate: 0.1,
+ sampleRate: 0.01,
};
private lastClickTime: number = 0;
@@ -35,14 +35,20 @@ class BehaviorPlugin implements LightPlugin {
private pageEnterTime: number = 0;
private scrollReported: boolean = false;
+ private originalPushState?: typeof history.pushState;
+ private originalReplaceState?: typeof history.replaceState;
+ private onScroll?: () => void;
+ private onClick?: (e: Event) => void;
+ private onVisibilityChange?: () => void;
+ private onBeforeUnload?: () => void;
+ private onPagehide?: () => void;
+ private onPopState?: () => void;
+ private onHashchange?: () => void;
+
setup(client: LightClient): void {
this.client = client;
this.loadConfig();
- if (!this.shouldSample()) {
- return;
- }
-
if (this.config.capturePV) {
this.trackPV();
}
@@ -68,7 +74,7 @@ class BehaviorPlugin implements LightPlugin {
}
private shouldSample(): boolean {
- const rate = this.config.sampleRate ?? 0.1;
+ const rate = this.config.sampleRate ?? 0.01;
if (rate >= 1) return true;
if (rate <= 0) return false;
return Math.random() < rate;
@@ -90,7 +96,7 @@ class BehaviorPlugin implements LightPlugin {
if (typeof window === 'undefined') return;
const url = getPageUrl();
- const referrer = document.referrer;
+ const referrer = sanitizeUrl(document.referrer || '');
this.reportBehavior({
sub_type: 'pv',
@@ -111,7 +117,7 @@ class BehaviorPlugin implements LightPlugin {
private trackClick(): void {
if (typeof document === 'undefined') return;
- document.addEventListener('click', (e) => {
+ this.onClick = (e: Event) => {
const currentTime = now();
const throttle = this.config.clickThrottle ?? 300;
if (currentTime - this.lastClickTime < throttle) {
@@ -142,11 +148,13 @@ class BehaviorPlugin implements LightPlugin {
selector,
text: text || undefined,
tag: target.tagName?.toLowerCase(),
- x: e.clientX,
- y: e.clientY,
+ x: (e as MouseEvent).clientX,
+ y: (e as MouseEvent).clientY,
},
});
- }, true);
+ };
+
+ document.addEventListener('click', this.onClick, true);
}
private trackRoute(): void {
@@ -177,28 +185,27 @@ class BehaviorPlugin implements LightPlugin {
}
};
- const originalPushState = history.pushState;
- const originalReplaceState = history.replaceState;
+ this.originalPushState = history.pushState;
+ this.originalReplaceState = history.replaceState;
+ const self = this;
history.pushState = function () {
- const result = originalPushState.apply(this, arguments as unknown as Parameters);
+ const result = self.originalPushState!.apply(this, arguments as unknown as Parameters);
setTimeout(checkRoute, 0);
return result;
};
history.replaceState = function () {
- const result = originalReplaceState.apply(this, arguments as unknown as Parameters);
+ const result = self.originalReplaceState!.apply(this, arguments as unknown as Parameters);
setTimeout(checkRoute, 0);
return result;
};
- window.addEventListener('popstate', () => {
- setTimeout(checkRoute, 0);
- });
+ this.onPopState = () => { setTimeout(checkRoute, 0); };
+ this.onHashchange = () => { setTimeout(checkRoute, 0); };
- window.addEventListener('hashchange', () => {
- setTimeout(checkRoute, 0);
- });
+ window.addEventListener('popstate', this.onPopState);
+ window.addEventListener('hashchange', this.onHashchange);
}
private trackPageDuration(): void {
@@ -224,22 +231,26 @@ class BehaviorPlugin implements LightPlugin {
}
};
- document.addEventListener('visibilitychange', () => {
+ this.onVisibilityChange = () => {
if (document.hidden) {
sendDuration();
} else {
this.pageEnterTime = now();
}
- });
+ };
- window.addEventListener('beforeunload', sendDuration);
- window.addEventListener('pagehide', sendDuration);
+ this.onBeforeUnload = sendDuration;
+ this.onPagehide = sendDuration;
+
+ document.addEventListener('visibilitychange', this.onVisibilityChange);
+ window.addEventListener('beforeunload', this.onBeforeUnload);
+ window.addEventListener('pagehide', this.onPagehide);
}
private trackScroll(): void {
if (typeof window === 'undefined') return;
- const onScroll = () => {
+ this.onScroll = () => {
const currentTime = now();
const throttle = this.config.scrollThrottle ?? 1000;
if (currentTime - this.lastScrollTime < throttle) {
@@ -253,7 +264,7 @@ class BehaviorPlugin implements LightPlugin {
}
};
- window.addEventListener('scroll', onScroll, { passive: true });
+ window.addEventListener('scroll', this.onScroll, { passive: true });
}
private reportScroll(): void {
@@ -270,6 +281,8 @@ class BehaviorPlugin implements LightPlugin {
}
private reportBehavior(data: Omit): void {
+ if (!this.shouldSample()) return;
+
const event: BehaviorEvent = {
type: 'behavior',
level: 'info',
@@ -284,7 +297,39 @@ class BehaviorPlugin implements LightPlugin {
this.client.captureEvent(event);
}
- destroy(): void {}
+ destroy(): void {
+ if (typeof window !== 'undefined') {
+ if (this.originalPushState) {
+ history.pushState = this.originalPushState;
+ }
+ if (this.originalReplaceState) {
+ history.replaceState = this.originalReplaceState;
+ }
+ if (this.onPopState) {
+ window.removeEventListener('popstate', this.onPopState);
+ }
+ if (this.onHashchange) {
+ window.removeEventListener('hashchange', this.onHashchange);
+ }
+ if (this.onBeforeUnload) {
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+ }
+ if (this.onPagehide) {
+ window.removeEventListener('pagehide', this.onPagehide);
+ }
+ if (this.onScroll) {
+ window.removeEventListener('scroll', this.onScroll);
+ }
+ }
+ if (typeof document !== 'undefined') {
+ if (this.onClick) {
+ document.removeEventListener('click', this.onClick, true);
+ }
+ if (this.onVisibilityChange) {
+ document.removeEventListener('visibilitychange', this.onVisibilityChange);
+ }
+ }
+ }
}
export default BehaviorPlugin;
diff --git a/src/plugins/ErrorPlugin.ts b/src/plugins/ErrorPlugin.ts
index 3b7a787..84f5d3a 100644
--- a/src/plugins/ErrorPlugin.ts
+++ b/src/plugins/ErrorPlugin.ts
@@ -1,15 +1,19 @@
import type { LightPlugin, LightClient, ErrorEvent, StackFrame, LightConfig } from '../types';
-import { now, isObject } from '../utils/helper';
+import { now, isObject, matchPatterns } from '../utils/helper';
import { computeFingerprint } from '../utils/hash';
-import { shouldIgnoreUrl } from '../utils/env';
+import { sanitizeUrl, shouldIgnoreUrl } from '../utils/env';
+import { parseStackTrace } from '../utils/stacktrace';
interface ErrorPluginConfig {
maxStackFrames?: number;
captureNodeModules?: boolean;
captureColumn?: boolean;
relativePathOnly?: boolean;
+ sampleRate?: number;
+ resourceSampleRate?: number;
ignoreErrors?: (string | RegExp)[];
ignoreUrls?: (string | RegExp)[];
+ includePaths?: (string | RegExp)[];
}
class ErrorPlugin implements LightPlugin {
@@ -22,8 +26,13 @@ class ErrorPlugin implements LightPlugin {
captureNodeModules: false,
captureColumn: true,
relativePathOnly: false,
+ resourceSampleRate: 0.1,
};
+ private originalOnError?: typeof window.onerror;
+ private onUnhandledRejection?: (event: PromiseRejectionEvent) => void;
+ private onResourceError?: (event: Event) => void;
+
setup(client: LightClient): void {
this.client = client;
this.loadConfig();
@@ -43,31 +52,61 @@ class ErrorPlugin implements LightPlugin {
if (clientConfig.ignoreUrls) {
this.config.ignoreUrls = [...(this.config.ignoreUrls || []), ...clientConfig.ignoreUrls];
}
+ if (clientConfig.includePaths) {
+ this.config.includePaths = [...(this.config.includePaths || []), ...clientConfig.includePaths];
+ }
}
private shouldIgnoreError(message: string): boolean {
- if (!this.config.ignoreErrors || !message) return false;
- return this.config.ignoreErrors.some(pattern => {
- if (typeof pattern === 'string') {
- return message.includes(pattern);
- }
- return pattern.test(message);
- });
+ if (!message) return false;
+ return matchPatterns(message, this.config.ignoreErrors || []);
}
private shouldIgnoreScriptUrl(url: string): boolean {
- return shouldIgnoreUrl(url, this.config.ignoreUrls);
+ return shouldIgnoreUrl(url, this.config.ignoreUrls || []);
+ }
+
+ private shouldIncludePath(frames: StackFrame[]): boolean {
+ const patterns = this.config.includePaths || [];
+ if (patterns.length === 0) return true;
+
+ return frames.some(frame =>
+ patterns.some(pattern => {
+ if (typeof pattern === 'string') {
+ return frame.filename?.includes(pattern);
+ }
+ return pattern.test(frame.filename || '');
+ })
+ );
+ }
+
+ private shouldSample(): boolean {
+ const rate = this.config.sampleRate ?? 1;
+ if (rate >= 1) return true;
+ if (rate <= 0) return false;
+ return Math.random() < rate;
+ }
+
+ private shouldSampleResource(): boolean {
+ const rate = this.config.resourceSampleRate ?? 0.1;
+ if (rate >= 1) return true;
+ if (rate <= 0) return false;
+ return Math.random() < rate;
}
private setupGlobalError(): void {
if (typeof window === 'undefined') return;
- const originalOnError = window.onerror;
+ this.originalOnError = window.onerror;
window.onerror = (message, url, lineno, colno, error) => {
try {
- if (originalOnError) {
- originalOnError.call(window, message, url, lineno, colno, error);
+ if (this.originalOnError) {
+ this.originalOnError.call(window, message, url, lineno, colno, error);
+ }
+
+ if (url && this.shouldIgnoreScriptUrl(url as string)) {
+ return false;
}
const errorEvent = this.buildErrorEvent(
@@ -91,9 +130,15 @@ class ErrorPlugin implements LightPlugin {
private setupUnhandledRejection(): void {
if (typeof window === 'undefined') return;
- window.addEventListener('unhandledrejection', (event) => {
+ this.onUnhandledRejection = (event) => {
try {
const reason = event.reason;
+ const sourceUrl = this.extractUrlFromReason(reason);
+
+ if (sourceUrl && this.shouldIgnoreScriptUrl(sourceUrl)) {
+ return;
+ }
+
const errorEvent = this.buildErrorEvent(reason);
if (errorEvent) {
@@ -110,13 +155,29 @@ class ErrorPlugin implements LightPlugin {
} catch (e) {
console.error('[LightSDK] ErrorPlugin unhandledrejection handler error:', e);
}
- });
+ };
+
+ window.addEventListener('unhandledrejection', this.onUnhandledRejection);
+ }
+
+ private extractUrlFromReason(reason: unknown): string | null {
+ if (reason instanceof Error && reason.stack) {
+ const match = reason.stack.match(/at\s+(?:.+?\s+)?\((https?:\/\/[^:]+):\d+:\d+\)/);
+ if (match) {
+ return match[1];
+ }
+ const urlMatch = reason.stack.match(/(https?:\/\/[^\s)]+):\d+:\d+/);
+ if (urlMatch) {
+ return urlMatch[1];
+ }
+ }
+ return null;
}
private setupResourceError(): void {
if (typeof window === 'undefined') return;
- window.addEventListener('error', (event) => {
+ this.onResourceError = (event) => {
const target = event.target;
if (!target) return;
@@ -125,14 +186,20 @@ class ErrorPlugin implements LightPlugin {
if (tagName && src && ['img', 'script', 'link', 'audio', 'video'].includes(tagName)) {
try {
+ if (this.shouldIgnoreScriptUrl(src)) return;
+ if (!this.shouldSampleResource()) return;
+
+ const fingerprint = `resource:${tagName}`;
+ const sanitizedSrc = sanitizeUrl(src);
this.client.captureEvent({
type: 'error',
level: 'warning',
- message: `Resource load failed: ${tagName} ${src}`,
+ message: `Resource load failed: ${tagName} ${sanitizedSrc}`,
timestamp: now(),
+ fingerprint,
tags: {
resource_type: tagName,
- resource_url: src,
+ resource_url: sanitizedSrc,
},
exception: {
type: 'ResourceError',
@@ -143,7 +210,9 @@ class ErrorPlugin implements LightPlugin {
console.error('[LightSDK] ErrorPlugin resource error handler error:', e);
}
}
- }, true);
+ };
+
+ window.addEventListener('error', this.onResourceError, true);
}
private buildErrorEvent(error: Error | unknown, url?: string, lineno?: number, colno?: number): Partial & { type: string } | null {
@@ -152,7 +221,16 @@ class ErrorPlugin implements LightPlugin {
if (error instanceof Error) {
if (this.shouldIgnoreError(error.message)) return null;
- const frames = this.parseStackTrace(error.stack);
+ const frames = parseStackTrace(error.stack, {
+ captureNodeModules: this.config.captureNodeModules,
+ captureColumn: this.config.captureColumn,
+ relativePathOnly: this.config.relativePathOnly,
+ maxFrames: this.config.maxStackFrames,
+ });
+
+ if (!this.shouldIncludePath(frames)) return null;
+ if (!this.shouldSample()) return null;
+
const fingerprint = computeFingerprint(error.name, error.message, frames);
return {
@@ -164,7 +242,7 @@ class ErrorPlugin implements LightPlugin {
type: error.name,
value: error.message,
stacktrace: {
- frames: frames.slice(0, this.config.maxStackFrames || 5),
+ frames,
},
},
fingerprint,
@@ -184,6 +262,9 @@ class ErrorPlugin implements LightPlugin {
});
}
+ if (!this.shouldIncludePath(frames)) return null;
+ if (!this.shouldSample()) return null;
+
return {
type: 'error',
level: 'error',
@@ -199,64 +280,19 @@ class ErrorPlugin implements LightPlugin {
};
}
- private parseStackTrace(stack?: string): StackFrame[] {
- if (!stack) return [];
-
- const frames: StackFrame[] = [];
- const lines = stack.split('\n');
- const origin = typeof location !== 'undefined' ? `${location.protocol}//${location.host}` : '';
-
- for (const line of lines) {
- const match = line.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);
- if (match) {
- const [, fn, filename, lineNum, colNum] = match;
-
- const isNodeModules = filename.includes('node_modules');
- if (!this.config.captureNodeModules && isNodeModules) {
- continue;
- }
-
- let finalFilename = filename;
- if (this.config.relativePathOnly && origin && filename.startsWith(origin)) {
- finalFilename = filename.substring(origin.length);
- }
-
- frames.push({
- filename: finalFilename,
- function: fn,
- lineno: parseInt(lineNum, 10),
- colno: this.config.captureColumn ? parseInt(colNum, 10) : undefined,
- in_app: !isNodeModules,
- });
- } else {
- const urlMatch = line.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);
- if (urlMatch) {
- const [, filename, lineNum, colNum] = urlMatch;
-
- const isNodeModules = filename.includes('node_modules');
- if (!this.config.captureNodeModules && isNodeModules) {
- continue;
- }
-
- let finalFilename = filename;
- if (this.config.relativePathOnly && origin && filename.startsWith(origin)) {
- finalFilename = filename.substring(origin.length);
- }
-
- frames.push({
- filename: finalFilename,
- lineno: parseInt(lineNum, 10),
- colno: this.config.captureColumn ? parseInt(colNum, 10) : undefined,
- in_app: !isNodeModules,
- });
- }
+ destroy(): void {
+ if (typeof window !== 'undefined') {
+ if (this.originalOnError !== undefined) {
+ window.onerror = this.originalOnError;
+ }
+ if (this.onUnhandledRejection) {
+ window.removeEventListener('unhandledrejection', this.onUnhandledRejection);
+ }
+ if (this.onResourceError) {
+ window.removeEventListener('error', this.onResourceError, true);
}
}
-
- return frames.reverse();
}
-
- destroy(): void {}
}
export default ErrorPlugin;
diff --git a/src/plugins/NetworkPlugin.ts b/src/plugins/NetworkPlugin.ts
index fa35b9e..5e3140f 100644
--- a/src/plugins/NetworkPlugin.ts
+++ b/src/plugins/NetworkPlugin.ts
@@ -4,6 +4,8 @@ import { sanitizeUrl, shouldIgnoreUrl } from '../utils/env';
interface NetworkPluginConfig {
ignoreUrls?: (string | RegExp)[];
+ captureXHR?: boolean;
+ captureFetch?: boolean;
captureSuccess?: boolean;
captureBody?: boolean;
captureRequestBody?: boolean;
@@ -23,6 +25,8 @@ class NetworkPlugin implements LightPlugin {
private client!: LightClient;
private config: NetworkPluginConfig = {
+ captureXHR: true,
+ captureFetch: true,
captureSuccess: false,
captureBody: false,
captureRequestBody: false,
@@ -43,8 +47,12 @@ class NetworkPlugin implements LightPlugin {
setup(client: LightClient): void {
this.client = client;
this.loadConfig();
- this.patchFetch();
- this.patchXHR();
+ if (this.config.captureFetch) {
+ this.patchFetch();
+ }
+ if (this.config.captureXHR) {
+ this.patchXHR();
+ }
}
private loadConfig(): void {
@@ -151,6 +159,7 @@ class NetworkPlugin implements LightPlugin {
XMLHttpRequest.prototype.open = function (method: string, url: string | URL) {
(this as { _lightMethod?: string })._lightMethod = method;
(this as { _lightUrl?: string })._lightUrl = typeof url === 'string' ? url : url.toString();
+ (this as { _lightTracked?: boolean })._lightTracked = false;
return self.originalXHROpen.apply(this, arguments as unknown as Parameters);
};
@@ -158,67 +167,59 @@ class NetworkPlugin implements LightPlugin {
const startTime = now();
const method = (this as { _lightMethod?: string })._lightMethod || 'GET';
const url = (this as { _lightUrl?: string })._lightUrl || '';
+ const tracked = (this as { _lightTracked?: boolean })._lightTracked;
- if (self.shouldIgnore(url)) {
+ if (self.shouldIgnore(url) || tracked) {
return self.originalXHRSend.apply(this, arguments as unknown as Parameters);
}
+ (this as { _lightTracked?: boolean })._lightTracked = true;
const sanitizedUrl = sanitizeUrl(url);
let requestSize = 0;
if (body && typeof body === 'string') {
requestSize = body.length;
}
- const onLoadEnd = () => {
+ const originalOnLoadEnd = (this as XMLHttpRequest & { onloadend?: ((this: XMLHttpRequest, ev: ProgressEvent) => unknown) | null }).onloadend;
+ (this as XMLHttpRequest & { onloadend?: ((this: XMLHttpRequest, ev: ProgressEvent) => unknown) | null }).onloadend = function (ev: ProgressEvent) {
const duration = now() - startTime;
const status = this.status;
const isSuccess = status >= 200 && status < 300;
- if (self.config.ignoreStatusCodes?.includes(status)) {
- this.removeEventListener('loadend', onLoadEnd);
- return;
- }
-
- if (!self.shouldSample(!isSuccess, duration)) {
- this.removeEventListener('loadend', onLoadEnd);
- return;
- }
-
- if (isSuccess && !self.config.captureSuccess) {
- this.removeEventListener('loadend', onLoadEnd);
- return;
- }
-
- let responseSize = 0;
- try {
- const sizeHeader = this.getResponseHeader('content-length');
- if (sizeHeader) {
- responseSize = parseInt(sizeHeader, 10);
+ if (!self.config.ignoreStatusCodes?.includes(status)
+ && self.shouldSample(!isSuccess, duration)
+ && (isSuccess ? self.config.captureSuccess : true)) {
+ let responseSize = 0;
+ try {
+ const sizeHeader = this.getResponseHeader('content-length');
+ if (sizeHeader) {
+ responseSize = parseInt(sizeHeader, 10);
+ }
+ if (!responseSize && this.responseText) {
+ responseSize = this.responseText.length;
+ }
+ } catch {
+ // ignore
}
- if (!responseSize && this.responseText) {
- responseSize = this.responseText.length;
- }
- } catch {
- // ignore
+
+ self.reportNetwork({
+ sub_type: 'xhr',
+ method: method.toUpperCase(),
+ url: sanitizedUrl,
+ status_code: status,
+ duration,
+ request_size: requestSize || undefined,
+ response_size: responseSize || undefined,
+ success: isSuccess,
+ error: !isSuccess ? `HTTP ${status}` : undefined,
+ });
}
- self.reportNetwork({
- sub_type: 'xhr',
- method: method.toUpperCase(),
- url: sanitizedUrl,
- status_code: status,
- duration,
- request_size: requestSize || undefined,
- response_size: responseSize || undefined,
- success: isSuccess,
- error: !isSuccess ? `HTTP ${status}` : undefined,
- });
-
- this.removeEventListener('loadend', onLoadEnd);
+ if (originalOnLoadEnd) {
+ return originalOnLoadEnd.call(this, ev);
+ }
};
- this.addEventListener('loadend', onLoadEnd);
-
return self.originalXHRSend.apply(this, arguments as unknown as Parameters);
};
}
diff --git a/src/plugins/OfflinePlugin.ts b/src/plugins/OfflinePlugin.ts
index 96a7748..eb789bf 100644
--- a/src/plugins/OfflinePlugin.ts
+++ b/src/plugins/OfflinePlugin.ts
@@ -20,6 +20,8 @@ class OfflinePlugin implements LightPlugin {
private db: IDBDatabase | null = null;
private isOnline: boolean = true;
private isSyncing: boolean = false;
+ private onOnline?: () => void;
+ private onOffline?: () => void;
private pendingEvents: SentryEvent[] = [];
setup(client: LightClient): void {
@@ -57,10 +59,15 @@ class OfflinePlugin implements LightPlugin {
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
+ const currentVersion = event.oldVersion;
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
+
+ // 未来版本迁移可以在这里处理:
+ // if (currentVersion < 2) { ... }
};
});
}
@@ -70,16 +77,19 @@ class OfflinePlugin implements LightPlugin {
this.isOnline = navigator.onLine;
- window.addEventListener('online', () => {
+ this.onOnline = () => {
console.info('[LightSDK] OfflinePlugin: Network online');
this.isOnline = true;
this.syncPendingEvents();
- });
+ };
- window.addEventListener('offline', () => {
+ this.onOffline = () => {
console.info('[LightSDK] OfflinePlugin: Network offline');
this.isOnline = false;
- });
+ };
+
+ window.addEventListener('online', this.onOnline);
+ window.addEventListener('offline', this.onOffline);
}
private generateId(): string {
@@ -89,32 +99,41 @@ class OfflinePlugin implements LightPlugin {
private async saveToIndexedDB(event: SentryEvent): Promise {
if (!this.db) return;
+ const maxEvents = this.config.maxEvents || MAX_EVENTS;
+
+ const count = await this.getEventCount();
+ if (count >= maxEvents) {
+ await this.deleteOldestEvents(count - maxEvents + 1);
+ }
+
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
- // 先检查数量
- const countRequest = store.count();
- countRequest.onsuccess = async () => {
- const count = countRequest.result;
- if (count >= (this.config.maxEvents || MAX_EVENTS)) {
- // 删除最旧的事件
- await this.deleteOldestEvents(count - (this.config.maxEvents || MAX_EVENTS) + 1);
- }
-
- const offlineEvent: OfflineEvent = {
- id: this.generateId(),
- event: {
- ...event,
- offline: true, // 标记为离线事件
- } as SentryEvent,
- timestamp: now(),
- };
-
- const addRequest = store.add(offlineEvent);
- addRequest.onsuccess = () => resolve();
- addRequest.onerror = () => reject(addRequest.error);
+ const offlineEvent: OfflineEvent = {
+ id: this.generateId(),
+ event: {
+ ...event,
+ offline: true,
+ } as unknown as SentryEvent,
+ timestamp: now(),
};
+
+ const addRequest = store.add(offlineEvent);
+ addRequest.onsuccess = () => resolve();
+ addRequest.onerror = () => reject(addRequest.error);
+ });
+ }
+
+ private getEventCount(): Promise {
+ if (!this.db) return Promise.resolve(0);
+
+ return new Promise((resolve, reject) => {
+ const transaction = this.db!.transaction([STORE_NAME], 'readonly');
+ const store = transaction.objectStore(STORE_NAME);
+ const countRequest = store.count();
+ countRequest.onsuccess = () => resolve(countRequest.result);
+ countRequest.onerror = () => reject(countRequest.error);
});
}
@@ -184,42 +203,43 @@ class OfflinePlugin implements LightPlugin {
console.info('[LightSDK] OfflinePlugin: Syncing pending events');
try {
- const storedEvents = await this.getAllStoredEvents();
+ let syncedCount = 0;
- if (storedEvents.length === 0) {
- console.info('[LightSDK] OfflinePlugin: No pending events to sync');
- this.isSyncing = false;
- return;
+ // 先处理内存中的事件
+ while (this.pendingEvents.length > 0) {
+ const event = this.pendingEvents.shift() as Partial & { type: string };
+ try {
+ this.client.captureEvent(event);
+ syncedCount++;
+ } catch (error) {
+ console.error('[LightSDK] OfflinePlugin: Failed to re-enqueue memory event', error);
+ }
}
- // 分批上报,每次最多 50 条
- const batchSize = 50;
- for (let i = 0; i < storedEvents.length; i += batchSize) {
- const batch = storedEvents.slice(i, i + batchSize);
- const events = batch.map(e => e.event);
-
- try {
- // 使用客户端的上报接口
- const flush = async () => {
- return new Promise((resolve, reject) => {
- // 创建一个临时的上报函数
- // 注意:这里需要通过 EventBus 触发上报
- this.client.emit('sync:offline', events);
- resolve();
- });
- };
-
- await flush();
- console.info(`[LightSDK] OfflinePlugin: Synced ${batch.length} events`);
- } catch (error) {
- console.error('[LightSDK] OfflinePlugin: Failed to sync batch', error);
- // 继续同步其他批次
+ // 再处理 IndexedDB 中的事件
+ const storedEvents = await this.getAllStoredEvents();
+ if (storedEvents.length > 0) {
+ for (const offlineEvent of storedEvents) {
+ try {
+ const event = offlineEvent.event as Partial & { type: string };
+ this.client.captureEvent(event);
+ syncedCount++;
+ } catch (error) {
+ console.error('[LightSDK] OfflinePlugin: Failed to re-enqueue event', error);
+ }
}
}
+ // 等待队列 flush
+ try {
+ await this.client.flush();
+ } catch (e) {
+ // flush 失败不影响,事件已在队列中会自动重试
+ }
+
// 清空已同步的事件
await this.clearStoredEvents();
- console.info('[LightSDK] OfflinePlugin: All pending events synced');
+ console.info(`[LightSDK] OfflinePlugin: Synced ${syncedCount} events`);
} catch (error) {
console.error('[LightSDK] OfflinePlugin: Sync failed', error);
} finally {
@@ -229,22 +249,51 @@ class OfflinePlugin implements LightPlugin {
beforeReport(event: SentryEvent): SentryEvent | null {
if (!this.isOnline) {
- // 离线时保存到 IndexedDB
- this.saveToIndexedDB(event).catch(err => {
- console.error('[LightSDK] OfflinePlugin: Failed to save event offline', err);
- });
- // 返回 null 表示不上报(因为已经缓存了)
+ const maxEvents = this.config.maxEvents || MAX_EVENTS;
+ if (this.pendingEvents.length >= maxEvents) {
+ this.pendingEvents.shift();
+ }
+ this.pendingEvents.push(event);
+ this.savePendingEventsToDB();
return null;
}
- // 在线时,标记为非离线事件
const onlineEvent = { ...event };
delete (onlineEvent as Record).offline;
return onlineEvent as SentryEvent;
}
+ private savePendingEventsToDB(): void {
+ if (this.pendingEvents.length === 0) return;
+
+ const events = this.pendingEvents.splice(0, this.pendingEvents.length);
+ let savedCount = 0;
+
+ const saveNext = () => {
+ if (savedCount >= events.length) return;
+ const event = events[savedCount];
+ this.saveToIndexedDB(event).then(() => {
+ savedCount++;
+ saveNext();
+ }).catch(err => {
+ console.error('[LightSDK] OfflinePlugin: Failed to save event offline', err);
+ // 恢复剩余未保存的事件
+ this.pendingEvents.unshift(...events.slice(savedCount));
+ });
+ };
+
+ saveNext();
+ }
+
destroy(): void {
- // 清理资源
+ if (typeof window !== 'undefined') {
+ if (this.onOnline) {
+ window.removeEventListener('online', this.onOnline);
+ }
+ if (this.onOffline) {
+ window.removeEventListener('offline', this.onOffline);
+ }
+ }
if (this.db) {
this.db.close();
this.db = null;
diff --git a/src/plugins/PerformancePlugin.ts b/src/plugins/PerformancePlugin.ts
index 209640f..1001dcb 100644
--- a/src/plugins/PerformancePlugin.ts
+++ b/src/plugins/PerformancePlugin.ts
@@ -26,14 +26,19 @@ class PerformancePlugin implements LightPlugin {
captureFP: true,
};
+ private observers: PerformanceObserver[] = [];
+ private onLoad?: () => void;
+ private navLoadTimer: ReturnType | null = null;
+ private totalBlockingTime: number = 0;
+ private fcpTime: number | null = null;
+ private lastLongTaskEndTime: number = 0;
+ private ttiReported: boolean = false;
+ private ttiTimer: ReturnType | null = null;
+
setup(client: LightClient): void {
this.client = client;
this.loadConfig();
- if (!this.shouldSample('default')) {
- return;
- }
-
this.observeWebVitals();
this.observeLongTasks();
this.observeNavigation();
@@ -47,12 +52,14 @@ class PerformancePlugin implements LightPlugin {
}
}
- private shouldSample(type: 'default' | 'resource' | 'longtask' = 'default'): boolean {
+ private shouldSample(type: 'default' | 'resource' | 'longtask' | 'vitals' = 'default'): boolean {
let rate = this.config.sampleRate ?? 0.1;
if (type === 'resource') {
rate = this.config.resourceSampleRate ?? 0.01;
} else if (type === 'longtask') {
rate = this.config.longTaskSampleRate ?? 0.05;
+ } else if (type === 'vitals') {
+ rate = 1;
}
if (rate >= 1) return true;
if (rate <= 0) return false;
@@ -77,10 +84,11 @@ class PerformancePlugin implements LightPlugin {
const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByName('first-paint');
if (entries.length > 0) {
- this.reportMetric('FP', entries[0].startTime, 'ms');
+ this.reportMetric('FP', entries[0].startTime, 'ms', {}, 'vitals');
}
});
po.observe({ type: 'paint', buffered: true });
+ this.observers.push(po);
} catch {
// FP not supported
}
@@ -93,10 +101,11 @@ class PerformancePlugin implements LightPlugin {
const lastEntry = entries[entries.length - 1] as PerformanceEntry & { renderTime?: number; loadTime?: number };
if (lastEntry) {
const value = lastEntry.renderTime || lastEntry.loadTime || lastEntry.startTime;
- this.reportMetric('LCP', value, 'ms');
+ this.reportMetric('LCP', value, 'ms', {}, 'vitals');
}
});
po.observe({ type: 'largest-contentful-paint', buffered: true });
+ this.observers.push(po);
} catch {
// LCP not supported
}
@@ -109,10 +118,11 @@ class PerformancePlugin implements LightPlugin {
if (entries.length > 0) {
const firstEntry = entries[0];
const value = firstEntry.processingStart - firstEntry.startTime;
- this.reportMetric('FID', value, 'ms');
+ this.reportMetric('FID', value, 'ms', {}, 'vitals');
}
});
po.observe({ type: 'first-input', buffered: true });
+ this.observers.push(po);
} catch {
// FID not supported
}
@@ -125,7 +135,7 @@ class PerformancePlugin implements LightPlugin {
let sessionEntries: PerformanceEntry[] = [];
const po = new PerformanceObserver((entryList) => {
- const entries = entryList.getEntries() as PerformanceEntry & { hadRecentInput?: boolean; value?: number }[];
+ const entries = entryList.getEntries() as unknown as { hadRecentInput?: boolean; value?: number; startTime: number; endTime?: number }[];
for (const entry of entries) {
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
@@ -133,24 +143,25 @@ class PerformancePlugin implements LightPlugin {
if (
sessionValue &&
- entry.startTime - (lastSessionEntry as { endTime?: number }).endTime! < 1000 &&
+ entry.startTime - (lastSessionEntry?.duration || 0) < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value || 0;
- sessionEntries.push(entry as PerformanceEntry);
+ sessionEntries.push(entry as unknown as PerformanceEntry);
} else {
sessionValue = entry.value || 0;
- sessionEntries = [entry as PerformanceEntry];
+ sessionEntries = [entry as unknown as PerformanceEntry];
}
if (sessionValue > clsValue) {
clsValue = sessionValue;
- this.reportMetric('CLS', clsValue, '');
+ this.reportMetric('CLS', clsValue, '', {}, 'vitals');
}
}
}
});
po.observe({ type: 'layout-shift', buffered: true });
+ this.observers.push(po);
} catch {
// CLS not supported
}
@@ -161,10 +172,14 @@ class PerformancePlugin implements LightPlugin {
const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntriesByName('first-contentful-paint');
if (entries.length > 0) {
- this.reportMetric('FCP', entries[0].startTime, 'ms');
+ this.fcpTime = entries[0].startTime;
+ this.lastLongTaskEndTime = entries[0].startTime;
+ this.reportMetric('FCP', entries[0].startTime, 'ms', {}, 'vitals');
+ this.scheduleTTI();
}
});
po.observe({ type: 'paint', buffered: true });
+ this.observers.push(po);
} catch {
// FCP not supported
}
@@ -177,10 +192,11 @@ class PerformancePlugin implements LightPlugin {
if (navEntries.length > 0) {
const navEntry = navEntries[0];
const ttfb = navEntry.responseStart - navEntry.requestStart;
- this.reportMetric('TTFB', Math.max(0, ttfb), 'ms');
+ this.reportMetric('TTFB', Math.max(0, ttfb), 'ms', {}, 'vitals');
}
});
po.observe({ type: 'navigation', buffered: true });
+ this.observers.push(po);
} catch {
// Navigation timing not supported
}
@@ -193,23 +209,53 @@ class PerformancePlugin implements LightPlugin {
const po = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
for (const entry of entries) {
- if (this.shouldSample('longtask')) {
- this.reportMetric('longtask', entry.duration, 'ms');
+ const endTime = entry.startTime + entry.duration;
+ if (endTime > this.lastLongTaskEndTime) {
+ this.lastLongTaskEndTime = endTime;
}
+
+ if (this.fcpTime !== null && entry.startTime >= this.fcpTime) {
+ const blockingTime = entry.duration - 50;
+ if (blockingTime > 0) {
+ this.totalBlockingTime += blockingTime;
+ }
+ }
+
+ this.reportMetric('longtask', entry.duration, 'ms', {}, 'longtask');
+
+ this.scheduleTTI();
}
});
po.observe({ type: 'longtask', buffered: true });
+ this.observers.push(po);
} catch {
// Long tasks not supported
}
}
+ private scheduleTTI(): void {
+ if (this.ttiReported) return;
+
+ if (this.ttiTimer) {
+ clearTimeout(this.ttiTimer);
+ }
+
+ this.ttiTimer = setTimeout(() => {
+ if (this.ttiReported) return;
+ this.ttiReported = true;
+
+ const tti = Math.max(this.lastLongTaskEndTime, this.fcpTime || 0);
+ this.reportMetric('TTI', tti, 'ms', {}, 'vitals');
+ this.reportMetric('TBT', this.totalBlockingTime, 'ms', {}, 'vitals');
+ }, 5000);
+ }
+
private observeNavigation(): void {
if (!this.config.captureNavigation) return;
if (typeof performance === 'undefined') return;
- window.addEventListener('load', () => {
- setTimeout(() => {
+ this.onLoad = () => {
+ this.navLoadTimer = setTimeout(() => {
const timing = performance.timing;
if (!timing) return;
@@ -230,8 +276,11 @@ class PerformancePlugin implements LightPlugin {
this.reportMetric(name, value, 'ms');
}
}
+ this.navLoadTimer = null;
}, 0);
- });
+ };
+
+ window.addEventListener('load', this.onLoad);
}
private observeResources(): void {
@@ -240,19 +289,18 @@ class PerformancePlugin implements LightPlugin {
try {
const po = new PerformanceObserver((entryList) => {
- if (!this.shouldSample('resource')) return;
-
const entries = entryList.getEntriesByType('resource') as PerformanceResourceTiming[];
for (const entry of entries) {
if (entry.duration > 1000) {
this.reportMetric('resource_slow', entry.duration, 'ms', {
resource_name: entry.name.substring(0, 200),
resource_type: entry.initiatorType,
- });
+ }, 'resource');
}
}
});
po.observe({ type: 'resource', buffered: true });
+ this.observers.push(po);
} catch {
// Resource timing not supported
}
@@ -276,7 +324,9 @@ class PerformancePlugin implements LightPlugin {
return 'poor';
}
- private reportMetric(name: string, value: number, unit: string, extraTags: Record = {}): void {
+ private reportMetric(name: string, value: number, unit: string, extraTags: Record = {}, sampleType: 'default' | 'resource' | 'longtask' | 'vitals' = 'default'): void {
+ if (!this.shouldSample(sampleType)) return;
+
const rating = this.getRating(name, value);
const event: PerformanceEvent = {
@@ -297,7 +347,30 @@ class PerformancePlugin implements LightPlugin {
this.client.captureEvent(event);
}
- destroy(): void {}
+ destroy(): void {
+ for (const observer of this.observers) {
+ try {
+ observer.disconnect();
+ } catch {
+ // ignore
+ }
+ }
+ this.observers = [];
+
+ if (this.ttiTimer) {
+ clearTimeout(this.ttiTimer);
+ this.ttiTimer = null;
+ }
+
+ if (this.navLoadTimer) {
+ clearTimeout(this.navLoadTimer);
+ this.navLoadTimer = null;
+ }
+
+ if (typeof window !== 'undefined' && this.onLoad) {
+ window.removeEventListener('load', this.onLoad);
+ }
+ }
}
export default PerformancePlugin;
diff --git a/src/plugins/SamplingPlugin.ts b/src/plugins/SamplingPlugin.ts
new file mode 100644
index 0000000..0771c3a
--- /dev/null
+++ b/src/plugins/SamplingPlugin.ts
@@ -0,0 +1,87 @@
+import type { LightPlugin, LightClient, SentryEvent, LightConfig } from '../types';
+import { isObject } from '../utils/helper';
+
+interface SampleRateConfig {
+ error?: number;
+ performance?: number;
+ network?: number;
+ behavior?: number;
+ [key: string]: number | undefined;
+}
+
+interface SamplingPluginConfig {
+ rates?: SampleRateConfig;
+}
+
+class SamplingPlugin implements LightPlugin {
+ name = 'sampling';
+ version = '1.0.0';
+
+ private client!: LightClient;
+ private config: SamplingPluginConfig = {
+ rates: {
+ error: 1,
+ performance: 1,
+ network: 1,
+ behavior: 1,
+ },
+ };
+
+ setup(client: LightClient): void {
+ this.client = client;
+ this.loadConfig();
+ }
+
+ private loadConfig(): void {
+ const clientConfig = this.client.config as LightConfig;
+ if (clientConfig && isObject(clientConfig.sampling)) {
+ const samplingConfig = clientConfig.sampling as object;
+ if ('rates' in samplingConfig && isObject((samplingConfig as { rates?: object }).rates)) {
+ this.config.rates = { ...this.config.rates, ...((samplingConfig as { rates: object }).rates as object) } as SampleRateConfig;
+ }
+ }
+ }
+
+ private shouldSample(event: SentryEvent): boolean {
+ const globalRate = (this.client.config as LightConfig).sampleRate ?? 1;
+ if (globalRate <= 0) return false;
+
+ const typeRate = this.getTypeRate(event);
+
+ if (globalRate >= 1) {
+ return this.randomCheck(typeRate);
+ }
+ return this.randomCheck(globalRate * typeRate);
+ }
+
+ private getTypeRate(event: SentryEvent): number {
+ const rates = this.config.rates || {};
+ const type = event.type;
+
+ if (type === 'count') {
+ return 1;
+ }
+
+ const rate = rates[type];
+ return rate !== undefined ? rate : 1;
+ }
+
+ private randomCheck(rate: number): boolean {
+ if (rate >= 1) return true;
+ if (rate <= 0) return false;
+ return Math.random() < rate;
+ }
+
+ beforeReport(event: SentryEvent): SentryEvent | null {
+ if (!this.shouldSample(event)) {
+ return null;
+ }
+ return event;
+ }
+
+ destroy(): void {
+ // nothing to clean up
+ }
+}
+
+export default SamplingPlugin;
diff --git a/src/types/index.ts b/src/types/index.ts
index 4647180..7e5ba3a 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,5 +1,12 @@
export type EventLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug';
+export interface ContextLevelConfig {
+ maxStackFrames: number;
+ maxBreadcrumbs: number;
+}
+
+export type ContextLevelMap = Record;
+
export interface UserInfo {
id?: string;
username?: string;
@@ -131,6 +138,16 @@ export interface DSNInfo {
projectId: string;
}
+export interface SamplingConfig {
+ rates?: {
+ error?: number;
+ performance?: number;
+ network?: number;
+ behavior?: number;
+ [key: string]: number | undefined;
+ };
+}
+
export interface LightConfig {
dsn: string;
release?: string;
@@ -144,9 +161,12 @@ export interface LightConfig {
ignoreErrors?: (string | RegExp)[];
ignoreUrls?: (string | RegExp)[];
includePaths?: (string | RegExp)[];
+ maxBreadcrumbs?: number;
beforeSend?: (event: SentryEvent) => SentryEvent | null;
user?: UserInfo;
plugins?: (LightPlugin | string)[];
+ contextLevel?: Partial;
+ sampling?: SamplingConfig;
[pluginName: string]: unknown;
}
@@ -176,4 +196,7 @@ export interface LightClient {
flush(): Promise;
disable(): void;
enable(): void;
+ use(plugin: LightPlugin): void;
+ init(): void;
+ destroy(): void;
}
diff --git a/src/utils/dsn.ts b/src/utils/dsn.ts
index acffe82..5fe487a 100644
--- a/src/utils/dsn.ts
+++ b/src/utils/dsn.ts
@@ -21,9 +21,9 @@ export function parseDSN(dsn: string): DSNInfo {
}
export function getEnvelopeUrl(dsn: DSNInfo): string {
- return `${dsn.protocol}://${dsn.host}/${dsn.projectId}/envelope/`;
+ return `${dsn.protocol}://${dsn.host}/api/${dsn.projectId}/envelope/?sentry_key=${dsn.publicKey}`;
}
export function getStoreUrl(dsn: DSNInfo): string {
- return `${dsn.protocol}://${dsn.host}/${dsn.projectId}/store/`;
+ return `${dsn.protocol}://${dsn.host}/api/${dsn.projectId}/store/?sentry_key=${dsn.publicKey}`;
}
diff --git a/src/utils/env.ts b/src/utils/env.ts
index 6deb008..a4456fa 100644
--- a/src/utils/env.ts
+++ b/src/utils/env.ts
@@ -186,6 +186,25 @@ export function sanitizeUrl(url: string): string {
}
}
+ if (urlObj.hash && urlObj.hash.length > 1) {
+ const hashContent = urlObj.hash.substring(1);
+ if (hashContent.includes('=')) {
+ const hashParams = new URLSearchParams(hashContent);
+ let hashModified = false;
+ for (const key of Array.from(hashParams.keys())) {
+ const lowerKey = key.toLowerCase().replace(/[-_]/g, '_');
+ if (sensitiveParams.some(s => lowerKey.includes(s))) {
+ hashParams.set(key, '[Filtered]');
+ hashModified = true;
+ }
+ }
+ if (hashModified) {
+ urlObj.hash = '#' + hashParams.toString();
+ modified = true;
+ }
+ }
+ }
+
if (modified) {
return urlObj.toString();
}
diff --git a/src/utils/hash.ts b/src/utils/hash.ts
index 246b45b..43783a5 100644
--- a/src/utils/hash.ts
+++ b/src/utils/hash.ts
@@ -8,16 +8,9 @@ export function hashString(str: string): number {
}
export function md5(str: string): string {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- const char = str.charCodeAt(i);
- hash = ((hash << 5) - hash) + char;
- hash = hash & hash;
- }
- return Math.abs(hash).toString(16).padStart(8, '0') +
- Math.abs(hash * 31).toString(16).padStart(8, '0') +
- Math.abs(hash * 17).toString(16).padStart(8, '0') +
- Math.abs(hash * 7).toString(16).padStart(8, '0');
+ // 使用 hashString 替代不安全的 md5
+ // 用于非加密场景(ID生成、指纹计算)
+ return hashString(str).toString(16);
}
export function computeFingerprint(type: string, message: string, frames: { filename: string; lineno?: number }[] = []): string {
diff --git a/src/utils/helper.ts b/src/utils/helper.ts
index 05ed89b..378a96b 100644
--- a/src/utils/helper.ts
+++ b/src/utils/helper.ts
@@ -40,3 +40,13 @@ export function truncate(str: string, maxLen: number): string {
if (str.length <= maxLen) return str;
return str.slice(0, maxLen) + '...';
}
+
+export function matchPatterns(value: string, patterns: (string | RegExp)[]): boolean {
+ if (!patterns || patterns.length === 0) return false;
+ return patterns.some(pattern => {
+ if (typeof pattern === 'string') {
+ return value.includes(pattern);
+ }
+ return pattern.test(value);
+ });
+}
diff --git a/src/utils/stacktrace.ts b/src/utils/stacktrace.ts
new file mode 100644
index 0000000..dd0abc4
--- /dev/null
+++ b/src/utils/stacktrace.ts
@@ -0,0 +1,100 @@
+import type { StackFrame } from '../types';
+
+export interface ParseStackOptions {
+ captureNodeModules?: boolean;
+ captureColumn?: boolean;
+ relativePathOnly?: boolean;
+ maxFrames?: number;
+}
+
+export function parseStackTrace(stack?: string, options: ParseStackOptions = {}): StackFrame[] {
+ if (!stack) return [];
+
+ const {
+ captureNodeModules = false,
+ captureColumn = true,
+ relativePathOnly = false,
+ maxFrames,
+ } = options;
+
+ const frames: StackFrame[] = [];
+ const lines = stack.split('\n');
+ const origin = typeof location !== 'undefined' ? `${location.protocol}//${location.host}` : '';
+
+ for (const line of lines) {
+ const match = line.match(/^\s*at\s*(.+?)\s*\((.+?):(\d+):(\d+)\)\s*$/);
+ if (match) {
+ const [, fn, filename, lineNum, colNum] = match;
+
+ const isNodeModules = filename.includes('node_modules');
+ if (!captureNodeModules && isNodeModules) {
+ continue;
+ }
+
+ let finalFilename = filename;
+ if (relativePathOnly && origin && filename.startsWith(origin)) {
+ finalFilename = filename.substring(origin.length);
+ }
+
+ frames.push({
+ filename: finalFilename,
+ function: fn,
+ lineno: parseInt(lineNum, 10),
+ colno: captureColumn ? parseInt(colNum, 10) : undefined,
+ in_app: !isNodeModules,
+ });
+ } else {
+ const v8Match = line.match(/^\s*at\s*(.+?)@(.+?):(\d+):(\d+)\s*$/);
+ if (v8Match) {
+ const [, fn, filename, lineNum, colNum] = v8Match;
+
+ const isNodeModules = filename.includes('node_modules');
+ if (!captureNodeModules && isNodeModules) {
+ continue;
+ }
+
+ let finalFilename = filename;
+ if (relativePathOnly && origin && filename.startsWith(origin)) {
+ finalFilename = filename.substring(origin.length);
+ }
+
+ frames.push({
+ filename: finalFilename,
+ function: fn || '',
+ lineno: parseInt(lineNum, 10),
+ colno: captureColumn ? parseInt(colNum, 10) : undefined,
+ in_app: !isNodeModules,
+ });
+ continue;
+ }
+
+ const urlMatch = line.match(/^\s*at\s*(.+?):(\d+):(\d+)\s*$/);
+ if (urlMatch) {
+ const [, filename, lineNum, colNum] = urlMatch;
+
+ const isNodeModules = filename.includes('node_modules');
+ if (!captureNodeModules && isNodeModules) {
+ continue;
+ }
+
+ let finalFilename = filename;
+ if (relativePathOnly && origin && filename.startsWith(origin)) {
+ finalFilename = filename.substring(origin.length);
+ }
+
+ frames.push({
+ filename: finalFilename,
+ lineno: parseInt(lineNum, 10),
+ colno: captureColumn ? parseInt(colNum, 10) : undefined,
+ in_app: !isNodeModules,
+ });
+ }
+ }
+ }
+
+ const reversed = frames.reverse();
+ if (maxFrames !== undefined && maxFrames > 0) {
+ return reversed.slice(0, maxFrames);
+ }
+ return reversed;
+}
diff --git a/tsconfig.json b/tsconfig.json
index 0ca4bc0..c9b4420 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -3,7 +3,7 @@
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "node",
- "lib": ["ES2018", "DOM", "DOM.Iterable"],
+ "lib": ["ES2019", "DOM", "DOM.Iterable"],
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,