From c8ab7ca0100a635528d759c20b1874e28f47b63d Mon Sep 17 00:00:00 2001 From: weidingjian Date: Wed, 24 Jun 2026 21:12:04 +0800 Subject: [PATCH] feat: light sentry sdk --- .gitignore | 4 + dist/core/Client.d.ts | 38 + dist/core/ConfigManager.d.ts | 33 + dist/core/EventBus.d.ts | 10 + dist/core/EventQueue.d.ts | 29 + dist/core/PluginManager.d.ts | 13 + dist/core/Reporter.d.ts | 43 + dist/index.cjs.js | 4 + dist/index.d.ts | 40 + dist/index.esm.js | 4 + dist/index.iife.js | 4 + dist/plugins/BehaviorPlugin.d.ts | 25 + dist/plugins/ErrorPlugin.d.ts | 18 + dist/plugins/NetworkPlugin.d.ts | 19 + dist/plugins/OfflinePlugin.d.ts | 24 + dist/plugins/PerformancePlugin.d.ts | 24 + dist/types/index.d.ts | 161 ++ dist/utils/dsn.d.ts | 4 + dist/utils/env.d.ts | 49 + dist/utils/field-encoder.d.ts | 23 + dist/utils/hash.d.ts | 6 + dist/utils/helper.d.ts | 8 + package-lock.json | 3065 +++++++++++++++++++++++++++ package.json | 34 + scripts/build.js | 125 ++ src/core/Client.ts | 301 +++ src/core/ConfigManager.ts | 183 ++ src/core/EventBus.ts | 47 + src/core/EventQueue.ts | 247 +++ src/core/PluginManager.ts | 87 + src/core/Reporter.ts | 336 +++ src/index.ts | 148 ++ src/plugins/BehaviorPlugin.ts | 290 +++ src/plugins/ErrorPlugin.ts | 262 +++ src/plugins/NetworkPlugin.ts | 255 +++ src/plugins/OfflinePlugin.ts | 255 +++ src/plugins/PerformancePlugin.ts | 303 +++ src/types/index.ts | 179 ++ src/utils/dsn.ts | 29 + src/utils/env.ts | 269 +++ src/utils/field-encoder.ts | 88 + src/utils/hash.ts | 27 + src/utils/helper.ts | 42 + tests/ConfigManager.test.ts | 248 +++ tests/dsn.test.ts | 57 + tests/env.test.ts | 157 ++ tests/hash.test.ts | 84 + tsconfig.json | 23 + vitest.config.ts | 15 + 49 files changed, 7739 insertions(+) create mode 100644 .gitignore create mode 100644 dist/core/Client.d.ts create mode 100644 dist/core/ConfigManager.d.ts create mode 100644 dist/core/EventBus.d.ts create mode 100644 dist/core/EventQueue.d.ts create mode 100644 dist/core/PluginManager.d.ts create mode 100644 dist/core/Reporter.d.ts create mode 100644 dist/index.cjs.js create mode 100644 dist/index.d.ts create mode 100644 dist/index.esm.js create mode 100644 dist/index.iife.js create mode 100644 dist/plugins/BehaviorPlugin.d.ts create mode 100644 dist/plugins/ErrorPlugin.d.ts create mode 100644 dist/plugins/NetworkPlugin.d.ts create mode 100644 dist/plugins/OfflinePlugin.d.ts create mode 100644 dist/plugins/PerformancePlugin.d.ts create mode 100644 dist/types/index.d.ts create mode 100644 dist/utils/dsn.d.ts create mode 100644 dist/utils/env.d.ts create mode 100644 dist/utils/field-encoder.d.ts create mode 100644 dist/utils/hash.d.ts create mode 100644 dist/utils/helper.d.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/build.js create mode 100644 src/core/Client.ts create mode 100644 src/core/ConfigManager.ts create mode 100644 src/core/EventBus.ts create mode 100644 src/core/EventQueue.ts create mode 100644 src/core/PluginManager.ts create mode 100644 src/core/Reporter.ts create mode 100644 src/index.ts create mode 100644 src/plugins/BehaviorPlugin.ts create mode 100644 src/plugins/ErrorPlugin.ts create mode 100644 src/plugins/NetworkPlugin.ts create mode 100644 src/plugins/OfflinePlugin.ts create mode 100644 src/plugins/PerformancePlugin.ts create mode 100644 src/types/index.ts create mode 100644 src/utils/dsn.ts create mode 100644 src/utils/env.ts create mode 100644 src/utils/field-encoder.ts create mode 100644 src/utils/hash.ts create mode 100644 src/utils/helper.ts create mode 100644 tests/ConfigManager.test.ts create mode 100644 tests/dsn.test.ts create mode 100644 tests/env.test.ts create mode 100644 tests/hash.test.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e68c2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/dist/core/Client.d.ts b/dist/core/Client.d.ts new file mode 100644 index 0000000..69e9164 --- /dev/null +++ b/dist/core/Client.d.ts @@ -0,0 +1,38 @@ +import type { LightConfig, LightClient, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb } from '../types'; +declare class Client implements LightClient { + config: LightConfig; + dsn: DSNInfo; + private configManager; + private eventBus; + private queue; + private reporter; + private pluginManager; + private breadcrumbs; + private maxBreadcrumbs; + private enabled; + constructor(config: LightConfig); + init(): void; + private flushEvents; + private syncFlushEvents; + on(event: string, handler: (...args: unknown[]) => void): void; + off(event: string, handler: (...args: unknown[]) => void): void; + emit(event: string, ...args: unknown[]): void; + captureException(error: Error | unknown): void; + captureMessage(message: string, level?: EventLevel): void; + captureEvent(event: Partial & { + type: string; + }): void; + private buildErrorEvent; + private parseStackTrace; + setUser(user: UserInfo | null): void; + setTag(key: string, value: string): void; + setTags(tags: Record): void; + setExtra(key: string, value: unknown): void; + addBreadcrumb(breadcrumb: Omit): void; + flush(): Promise; + disable(): void; + enable(): void; + use(plugin: unknown): void; + destroy(): void; +} +export default Client; diff --git a/dist/core/ConfigManager.d.ts b/dist/core/ConfigManager.d.ts new file mode 100644 index 0000000..812becc --- /dev/null +++ b/dist/core/ConfigManager.d.ts @@ -0,0 +1,33 @@ +import type { LightConfig, SentryEvent, UserInfo } from '../types'; +export declare class ConfigManager { + private config; + private contexts; + private contextId; + private contextReported; + constructor(config: LightConfig); + markContextReported(): void; + get(key: K): LightConfig[K]; + getAll(): LightConfig; + set(key: K, value: LightConfig[K]): void; + setUser(user: UserInfo | null): void; + setTags(tags: Record): void; + setTag(key: string, value: string): void; + setExtra(key: string, value: unknown): void; + shouldSample(): boolean; + isIgnoredError(message: string): boolean; + /** + * 获取编码后的环境信息(延迟初始化,只在首次上报时计算) + * 注意:env.ts 的 getContexts 已经返回编码后的数据 + */ + private getEncodedContext; + /** + * 将配置应用到事件 + * @param event 事件对象 + * @param includeFullContext 是否包含完整环境信息(批次内只有首个事件需要) + */ + applyToEvent(event: SentryEvent, includeFullContext?: boolean): SentryEvent; + /** + * 应用上下文分层策略:根据事件级别裁剪堆栈和 breadcrumbs + */ + private applyContextLevel; +} diff --git a/dist/core/EventBus.d.ts b/dist/core/EventBus.d.ts new file mode 100644 index 0000000..86594ef --- /dev/null +++ b/dist/core/EventBus.d.ts @@ -0,0 +1,10 @@ +type Handler = (...args: unknown[]) => void; +export declare class EventBus { + private handlers; + on(event: string, handler: Handler): void; + off(event: string, handler: Handler): void; + emit(event: string, ...args: unknown[]): void; + once(event: string, handler: Handler): void; + destroy(): void; +} +export {}; diff --git a/dist/core/EventQueue.d.ts b/dist/core/EventQueue.d.ts new file mode 100644 index 0000000..40edad0 --- /dev/null +++ b/dist/core/EventQueue.d.ts @@ -0,0 +1,29 @@ +import type { SentryEvent } from '../types'; +import { EventBus } from './EventBus'; +export declare class EventQueue { + private queue; + private maxSize; + private flushInterval; + private timer; + private eventBus; + private flushCallback; + private syncFlushCallback; + private lastFlushTime; + private dedupeMap; + private pendingCounts; + private errorRateWindow; + private paused; + private pauseTimer; + constructor(maxSize: number, flushInterval: number, eventBus: EventBus, flushCallback: (events: SentryEvent[]) => Promise, syncFlushCallback: (events: SentryEvent[]) => void); + start(): void; + private startTimer; + private setupVisibilityListener; + private checkInfiniteLoop; + enqueue(event: SentryEvent): void; + private buildCountEvents; + private getDedupeKey; + flush(): Promise; + flushSync(): void; + size(): number; + destroy(): void; +} diff --git a/dist/core/PluginManager.d.ts b/dist/core/PluginManager.d.ts new file mode 100644 index 0000000..ef2c536 --- /dev/null +++ b/dist/core/PluginManager.d.ts @@ -0,0 +1,13 @@ +import type { LightPlugin, LightClient, SentryEvent } from '../types'; +export declare class PluginManager { + private plugins; + private client; + constructor(client: LightClient); + add(plugin: LightPlugin): void; + remove(name: string): void; + get(name: string): LightPlugin | undefined; + has(name: string): boolean; + applyBeforeReport(event: SentryEvent): SentryEvent | null; + applyAfterReport(event: SentryEvent): void; + destroy(): void; +} diff --git a/dist/core/Reporter.d.ts b/dist/core/Reporter.d.ts new file mode 100644 index 0000000..9d91cd9 --- /dev/null +++ b/dist/core/Reporter.d.ts @@ -0,0 +1,43 @@ +import type { DSNInfo, SentryEvent } from '../types'; +export declare class Reporter { + private dsn; + private maxRetries; + private retryDelay; + private useShortFields; + constructor(dsn: DSNInfo, maxRetries: number, retryDelay: number); + report(events: SentryEvent[]): Promise; + reportSync(events: SentryEvent[]): void; + /** + * 构建 Sentry Envelope + * + * 优化: + * 1. 批量元数据共享 - 同批次共享 release/environment/user + * 2. 字段短编码 - 常用字段名使用短码 + * 3. 时间戳相对化 - 批次内后续事件用差值 + */ + private buildEnvelope; + /** + * 时间戳相对化 + * + * 批次内: + * - 第一个事件:完整时间戳 + * - 后续事件:相对于第一个事件的毫秒差值(用 _dts 字段表示) + * + * 这样可以将时间戳从 13 位数字减少到 2-4 位数字 + */ + private applyRelativeTimestamps; + /** + * 提取批次内公共元数据 + */ + private extractSharedMeta; + /** + * 移除已共享的字段,减少重复 + */ + private stripSharedFields; + private getEnvelopeType; + private generateEventId; + private send; + private sendSync; + private sendViaImage; + private delay; +} diff --git a/dist/index.cjs.js b/dist/index.cjs.js new file mode 100644 index 0000000..801442c --- /dev/null +++ b/dist/index.cjs.js @@ -0,0 +1,4 @@ +"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}; diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..86a8aff --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,40 @@ +import Client from './core/Client'; +import ErrorPlugin from './plugins/ErrorPlugin'; +import PerformancePlugin from './plugins/PerformancePlugin'; +import NetworkPlugin from './plugins/NetworkPlugin'; +import BehaviorPlugin from './plugins/BehaviorPlugin'; +import OfflinePlugin from './plugins/OfflinePlugin'; +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; +declare function captureException(error: Error | unknown): void; +declare function captureMessage(message: string, level?: EventLevel): void; +declare function captureEvent(event: Partial & { + type: string; +}): void; +declare function setUser(user: UserInfo | null): void; +declare function setTag(key: string, value: string): void; +declare function setTags(tags: Record): void; +declare function setExtra(key: string, value: unknown): void; +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 type { LightConfig, LightClient, LightPlugin, SentryEvent, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, EventLevel, UserInfo, Breadcrumb, DSNInfo, }; +declare const _default: { + init: typeof init; + getClient: typeof getClient; + captureException: typeof captureException; + captureMessage: typeof captureMessage; + captureEvent: typeof captureEvent; + setUser: typeof setUser; + setTag: typeof setTag; + setTags: typeof setTags; + setExtra: typeof setExtra; + addBreadcrumb: typeof addBreadcrumb; + flush: typeof flush; + disable: typeof disable; + enable: typeof enable; +}; +export default _default; diff --git a/dist/index.esm.js b/dist/index.esm.js new file mode 100644 index 0000000..2776006 --- /dev/null +++ b/dist/index.esm.js @@ -0,0 +1,4 @@ +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}; diff --git a/dist/index.iife.js b/dist/index.iife.js new file mode 100644 index 0000000..3f67a65 --- /dev/null +++ b/dist/index.iife.js @@ -0,0 +1,4 @@ +"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);})(); diff --git a/dist/plugins/BehaviorPlugin.d.ts b/dist/plugins/BehaviorPlugin.d.ts new file mode 100644 index 0000000..5fb85b0 --- /dev/null +++ b/dist/plugins/BehaviorPlugin.d.ts @@ -0,0 +1,25 @@ +import type { LightPlugin, LightClient } from '../types'; +declare class BehaviorPlugin implements LightPlugin { + name: string; + version: string; + private client; + private config; + private lastClickTime; + private lastScrollTime; + private maxScrollDepth; + private pageEnterTime; + private scrollReported; + setup(client: LightClient): void; + private loadConfig; + private shouldSample; + private getScrollDepth; + private trackPV; + private trackClick; + private trackRoute; + private trackPageDuration; + private trackScroll; + private reportScroll; + private reportBehavior; + destroy(): void; +} +export default BehaviorPlugin; diff --git a/dist/plugins/ErrorPlugin.d.ts b/dist/plugins/ErrorPlugin.d.ts new file mode 100644 index 0000000..f253909 --- /dev/null +++ b/dist/plugins/ErrorPlugin.d.ts @@ -0,0 +1,18 @@ +import type { LightPlugin, LightClient } from '../types'; +declare class ErrorPlugin implements LightPlugin { + name: string; + version: string; + private client; + private config; + setup(client: LightClient): void; + private loadConfig; + private shouldIgnoreError; + private shouldIgnoreScriptUrl; + private setupGlobalError; + private setupUnhandledRejection; + private setupResourceError; + private buildErrorEvent; + private parseStackTrace; + destroy(): void; +} +export default ErrorPlugin; diff --git a/dist/plugins/NetworkPlugin.d.ts b/dist/plugins/NetworkPlugin.d.ts new file mode 100644 index 0000000..ae9b34f --- /dev/null +++ b/dist/plugins/NetworkPlugin.d.ts @@ -0,0 +1,19 @@ +import type { LightPlugin, LightClient } from '../types'; +declare class NetworkPlugin implements LightPlugin { + name: string; + version: string; + private client; + private config; + private originalFetch; + private originalXHROpen; + private originalXHRSend; + setup(client: LightClient): void; + private loadConfig; + private shouldIgnore; + private shouldSample; + private patchFetch; + private patchXHR; + private reportNetwork; + destroy(): void; +} +export default NetworkPlugin; diff --git a/dist/plugins/OfflinePlugin.d.ts b/dist/plugins/OfflinePlugin.d.ts new file mode 100644 index 0000000..09b8e22 --- /dev/null +++ b/dist/plugins/OfflinePlugin.d.ts @@ -0,0 +1,24 @@ +import type { LightPlugin, LightClient, SentryEvent } from '../types'; +declare class OfflinePlugin implements LightPlugin { + name: string; + version: string; + private client; + private config; + private db; + private isOnline; + private isSyncing; + private pendingEvents; + setup(client: LightClient): void; + private loadConfig; + private initDatabase; + private setupNetworkListeners; + private generateId; + private saveToIndexedDB; + private deleteOldestEvents; + private getAllStoredEvents; + private clearStoredEvents; + syncPendingEvents(): Promise; + beforeReport(event: SentryEvent): SentryEvent | null; + destroy(): void; +} +export default OfflinePlugin; diff --git a/dist/plugins/PerformancePlugin.d.ts b/dist/plugins/PerformancePlugin.d.ts new file mode 100644 index 0000000..6f0d0de --- /dev/null +++ b/dist/plugins/PerformancePlugin.d.ts @@ -0,0 +1,24 @@ +import type { LightPlugin, LightClient } from '../types'; +declare class PerformancePlugin implements LightPlugin { + name: string; + version: string; + private client; + private config; + setup(client: LightClient): void; + private loadConfig; + private shouldSample; + private observeWebVitals; + private observeFP; + private observeLCP; + private observeFID; + private observeCLS; + private observeFCP; + private observeTTFB; + private observeLongTasks; + private observeNavigation; + private observeResources; + private getRating; + private reportMetric; + destroy(): void; +} +export default PerformancePlugin; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts new file mode 100644 index 0000000..09d825c --- /dev/null +++ b/dist/types/index.d.ts @@ -0,0 +1,161 @@ +export type EventLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug'; +export interface UserInfo { + id?: string; + username?: string; + email?: string; + [key: string]: unknown; +} +export interface StackFrame { + filename: string; + function?: string; + lineno?: number; + colno?: number; + in_app?: boolean; +} +export interface ExceptionInfo { + type: string; + value: string; + stacktrace?: { + frames: StackFrame[]; + }; +} +export interface RequestInfo { + url: string; + method?: string; + headers?: Record; + referrer?: string; +} +export interface BrowserContext { + name: string; + version?: string; +} +export interface OSContext { + name: string; + version?: string; +} +export interface DeviceContext { + family?: string; + model?: string; + brand?: string; + type?: string; +} +export interface Contexts { + browser?: BrowserContext; + os?: OSContext; + device?: DeviceContext; +} +export interface Breadcrumb { + type: string; + message: string; + timestamp: number; + category?: string; + data?: Record; + level?: EventLevel; +} +export interface BaseEvent { + type: string; + level: EventLevel; + timestamp: number; + release?: string; + environment?: string; + user?: UserInfo; + tags?: Record; + extra?: Record; + breadcrumbs?: Breadcrumb[]; + request?: RequestInfo; + contexts?: Contexts; + context_id?: string; +} +export interface ErrorEvent extends BaseEvent { + type: 'error'; + message: string; + exception?: ExceptionInfo; + fingerprint?: string; + title?: string; +} +export interface PerformanceEvent extends BaseEvent { + type: 'performance'; + metric: string; + value: number; + unit: string; + rating?: 'good' | 'needs-improvement' | 'poor'; +} +export interface NetworkEvent extends BaseEvent { + type: 'network'; + sub_type: 'fetch' | 'xhr'; + method: string; + url: string; + status_code?: number; + duration?: number; + request_size?: number; + response_size?: number; + success: boolean; + error?: string; +} +export interface BehaviorEvent extends BaseEvent { + type: 'behavior'; + sub_type: 'pv' | 'click' | 'route' | 'scroll' | 'duration'; + page_url: string; + referrer?: string; + properties?: Record; +} +export interface CountEvent extends BaseEvent { + type: 'count'; + fingerprint: string; + count: number; + ts_start: number; + ts_end: number; +} +export type SentryEvent = ErrorEvent | PerformanceEvent | NetworkEvent | BehaviorEvent | CountEvent; +export interface DSNInfo { + protocol: string; + publicKey: string; + host: string; + projectId: string; +} +export interface LightConfig { + dsn: string; + release?: string; + environment?: string; + enabled?: boolean; + sampleRate?: number; + maxQueueSize?: number; + flushInterval?: number; + maxRetries?: number; + retryDelay?: number; + ignoreErrors?: (string | RegExp)[]; + ignoreUrls?: (string | RegExp)[]; + includePaths?: (string | RegExp)[]; + beforeSend?: (event: SentryEvent) => SentryEvent | null; + user?: UserInfo; + plugins?: (LightPlugin | string)[]; + [pluginName: string]: unknown; +} +export interface LightPlugin { + name: string; + version: string; + setup(client: LightClient): void; + destroy?(): void; + beforeReport?(event: SentryEvent): SentryEvent | null; + afterReport?(event: SentryEvent): void; +} +export interface LightClient { + config: LightConfig; + dsn: DSNInfo; + on(event: string, handler: (...args: unknown[]) => void): void; + off(event: string, handler: (...args: unknown[]) => void): void; + emit(event: string, ...args: unknown[]): void; + captureException(error: Error | unknown): void; + captureMessage(message: string, level?: EventLevel): void; + captureEvent(event: Partial & { + type: string; + }): void; + setUser(user: UserInfo | null): void; + setTag(key: string, value: string): void; + setTags(tags: Record): void; + setExtra(key: string, value: unknown): void; + addBreadcrumb(breadcrumb: Omit): void; + flush(): Promise; + disable(): void; + enable(): void; +} diff --git a/dist/utils/dsn.d.ts b/dist/utils/dsn.d.ts new file mode 100644 index 0000000..17a03e0 --- /dev/null +++ b/dist/utils/dsn.d.ts @@ -0,0 +1,4 @@ +import type { DSNInfo } from '../types'; +export declare function parseDSN(dsn: string): DSNInfo; +export declare function getEnvelopeUrl(dsn: DSNInfo): string; +export declare function getStoreUrl(dsn: DSNInfo): string; diff --git a/dist/utils/env.d.ts b/dist/utils/env.d.ts new file mode 100644 index 0000000..0ad842c --- /dev/null +++ b/dist/utils/env.d.ts @@ -0,0 +1,49 @@ +export interface BrowserInfo { + name: string; + version: string; + major: string; +} +export interface OSInfo { + name: string; + version: string; +} +export interface DeviceInfo { + type: 'desktop' | 'mobile' | 'tablet'; + vendor: string; + model: string; +} +export interface EnvInfo { + browser: BrowserInfo; + os: OSInfo; + device: DeviceInfo; + url: string; + referrer: string; + title: string; + language: string; + user_agent: string; + screen_width: number; + screen_height: number; + viewport_width: number; + viewport_height: number; +} +export declare function getEnvInfo(): EnvInfo; +export declare function getPageUrl(): string; +export declare function getReferrer(): string; +export declare function sanitizeUrl(url: string): string; +export declare function shouldIgnoreUrl(url: string, ignoreUrls?: (string | RegExp)[]): boolean; +export declare function getContexts(): { + browser: { + name: string; + version: string; + }; + os: { + name: string; + version: string; + }; + device: { + type: "desktop" | "mobile" | "tablet"; + brand: string; + model: string; + }; +}; +export declare function getContextId(): string; diff --git a/dist/utils/field-encoder.d.ts b/dist/utils/field-encoder.d.ts new file mode 100644 index 0000000..d2a82b5 --- /dev/null +++ b/dist/utils/field-encoder.d.ts @@ -0,0 +1,23 @@ +/** + * 字段短编码映射 + * + * 将常用字段名编码为 2-3 个字符的短码,减少上报体积 + * + * 编码前:{ "type": "error", "level": "error", "message": "..." } + * 编码后:{ "t": "e", "l": "e", "m": "..." } + * + * 注意:只对最外层高频字段编码,不对嵌套对象深度编码 + * 避免复杂嵌套带来的解析问题和兼容性风险 + */ +export declare const FIELD_ENCODE_MAP: Record; +export declare const FIELD_DECODE_MAP: Record; +/** + * 编码事件字段名(SDK 端使用) + * 只对最外层字段编码,避免深度编码的复杂性 + */ +export declare function encodeFields(obj: Record): Record; +/** + * 解码事件字段名(服务端使用) + * 只对最外层字段解码 + */ +export declare function decodeFields(obj: Record): Record; diff --git a/dist/utils/hash.d.ts b/dist/utils/hash.d.ts new file mode 100644 index 0000000..1a27dec --- /dev/null +++ b/dist/utils/hash.d.ts @@ -0,0 +1,6 @@ +export declare function hashString(str: string): number; +export declare function md5(str: string): string; +export declare function computeFingerprint(type: string, message: string, frames?: { + filename: string; + lineno?: number; +}[]): string; diff --git a/dist/utils/helper.d.ts b/dist/utils/helper.d.ts new file mode 100644 index 0000000..792d994 --- /dev/null +++ b/dist/utils/helper.d.ts @@ -0,0 +1,8 @@ +export declare function generateEventId(): string; +export declare function now(): number; +export declare function uuid(): string; +export declare function isFunction(fn: unknown): fn is (...args: unknown[]) => unknown; +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; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..42c900e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3065 @@ +{ + "name": "light-sentry-sdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "light-sentry-sdk", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "esbuild": "^0.20.0", + "jsdom": "^24.0.0", + "typescript": "^5.4.0", + "vitest": "^1.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "24.1.3", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd5ba1e --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "light-sentry-sdk", + "version": "0.1.0", + "description": "Lightweight Sentry-compatible browser SDK", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "browser": "dist/index.iife.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "node scripts/build.js", + "dev": "node scripts/build.js --watch", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "keywords": [ + "sentry", + "monitoring", + "error-tracking", + "performance" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "esbuild": "^0.20.0", + "jsdom": "^24.0.0", + "typescript": "^5.4.0", + "vitest": "^1.4.0" + } +} diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..2c185a2 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,125 @@ +const esbuild = require('esbuild'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const isWatch = process.argv.includes('--watch'); + +const commonOptions = { + entryPoints: [path.resolve(__dirname, '../src/index.ts')], + bundle: true, + minify: true, + sourcemap: false, + target: ['es2018'], + logLevel: 'info', +}; + +// 生成 TypeScript 声明文件 +function generateDeclarations() { + console.log('Generating TypeScript declarations...'); + + const tmpDir = path.resolve(__dirname, '../dist-tmp'); + + try { + // 清理临时目录 + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + fs.mkdirSync(tmpDir, { recursive: true }); + + // 生成所有 .d.ts 到临时目录(宽松模式,忽略错误) + execSync( + `tsc --skipLibCheck --declaration --declarationDir ${tmpDir} --outDir ${tmpDir} --strict false --noImplicitAny false --strictNullChecks false --noEmitOnError false --emitDeclarationOnly`, + { + cwd: path.resolve(__dirname, '..'), + stdio: 'pipe', + } + ); + } catch (e) { + // 忽略 tsc 错误 + } + + // 只复制 .d.ts 文件到 dist,删除多余的 .js + copyDtsFiles(tmpDir, path.resolve(__dirname, '../dist')); + + // 清理临时目录 + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + + console.log('Declarations generated.'); +} + +// 递归复制 .d.ts 文件 +function copyDtsFiles(srcDir, destDir) { + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + const files = fs.readdirSync(srcDir); + for (const file of files) { + const srcPath = path.join(srcDir, file); + const stat = fs.statSync(srcPath); + + if (stat.isDirectory()) { + copyDtsFiles(srcPath, path.join(destDir, file)); + } else if (file.endsWith('.d.ts')) { + fs.copyFileSync(srcPath, path.join(destDir, file)); + } + } +} + +async function build() { + const distDir = path.resolve(__dirname, '../dist'); + + // 清理 dist 目录 + if (fs.existsSync(distDir)) { + fs.rmSync(distDir, { recursive: true, force: true }); + } + fs.mkdirSync(distDir, { recursive: true }); + + const outputs = [ + { + format: 'iife', + outfile: path.resolve(__dirname, '../dist/index.iife.js'), + globalName: 'LightSDK', + }, + { + format: 'esm', + outfile: path.resolve(__dirname, '../dist/index.esm.js'), + }, + { + format: 'cjs', + outfile: path.resolve(__dirname, '../dist/index.cjs.js'), + }, + ]; + + for (const output of outputs) { + await esbuild.build({ + ...commonOptions, + format: output.format, + outfile: output.outfile, + globalName: output.globalName, + }); + } + + // 生成声明文件 + generateDeclarations(); + + console.log('Build complete!'); +} + +if (isWatch) { + console.log('Watching for changes...'); + const ctx = esbuild.context({ + ...commonOptions, + format: 'esm', + outfile: path.resolve(__dirname, '../dist/index.esm.js'), + }); + ctx.watch(); +} else { + build().catch((e) => { + console.error(e); + process.exit(1); + }); +} diff --git a/src/core/Client.ts b/src/core/Client.ts new file mode 100644 index 0000000..28e81ed --- /dev/null +++ b/src/core/Client.ts @@ -0,0 +1,301 @@ +import type { LightConfig, LightClient, DSNInfo, SentryEvent, EventLevel, UserInfo, Breadcrumb, ErrorEvent } from '../types'; +import { parseDSN } from '../utils/dsn'; +import { now } from '../utils/helper'; +import { computeFingerprint } from '../utils/hash'; +import { ConfigManager } from './ConfigManager'; +import { EventBus } from './EventBus'; +import { EventQueue } from './EventQueue'; +import { Reporter } from './Reporter'; +import { PluginManager } from './PluginManager'; + +class Client implements LightClient { + config: LightConfig; + dsn: DSNInfo; + + private configManager: ConfigManager; + private eventBus: EventBus; + private queue: EventQueue; + private reporter: Reporter; + private pluginManager: PluginManager; + private breadcrumbs: Breadcrumb[] = []; + private maxBreadcrumbs = 20; + private enabled: boolean = true; + + constructor(config: LightConfig) { + if (!config.dsn) { + throw new Error('DSN is required'); + } + + 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.pluginManager = new PluginManager(this); + this.queue = new EventQueue( + config.maxQueueSize ?? 100, + config.flushInterval ?? 5000, + this.eventBus, + async (events) => this.flushEvents(events), + (events) => this.syncFlushEvents(events) + ); + + this.enabled = config.enabled !== false; + } + + init(): void { + if (!this.enabled) return; + this.queue.start(); + this.emit('ready'); + } + + private async flushEvents(events: SentryEvent[]): Promise { + const processedEvents: SentryEvent[] = []; + + for (const event of events) { + const processed = this.pluginManager.applyBeforeReport(event); + if (processed) { + processedEvents.push(processed); + } + } + + 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 + + const finalEvent = this.config.get('beforeSend') + ? this.config.get('beforeSend')!(withConfig) + : withConfig; + if (finalEvent) { + finalEvents.push(finalEvent); + } + } + + if (finalEvents.length === 0) return; + + await this.reporter.report(finalEvents); + // 批次结束后标记,后续批次不再带完整 contexts + this.configManager.markContextReported(); + + for (const event of finalEvents) { + this.pluginManager.applyAfterReport(event); + } + } + + private syncFlushEvents(events: SentryEvent[]): void { + const processedEvents: SentryEvent[] = []; + + for (const event of events) { + const processed = this.pluginManager.applyBeforeReport(event); + if (processed) { + processedEvents.push(processed); + } + } + + if (processedEvents.length === 0) return; + + // 批次内优化:只有第一个事件带完整环境信息 + let isFirstInBatch = true; + const finalEvents: SentryEvent[] = []; + + for (const event of processedEvents) { + 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); + } + } + + if (finalEvents.length === 0) return; + + this.reporter.reportSync(finalEvents); + this.configManager.markContextReported(); + } + + on(event: string, handler: (...args: unknown[]) => void): void { + this.eventBus.on(event, handler); + } + + off(event: string, handler: (...args: unknown[]) => void): void { + this.eventBus.off(event, handler); + } + + emit(event: string, ...args: unknown[]): void { + this.eventBus.emit(event, ...args); + } + + captureException(error: Error | unknown): void { + if (!this.enabled || !this.configManager.shouldSample()) return; + + const errorEvent = this.buildErrorEvent(error); + if (this.configManager.isIgnoredError(errorEvent.message)) return; + + errorEvent.breadcrumbs = [...this.breadcrumbs]; + this.queue.enqueue(errorEvent); + } + + captureMessage(message: string, level: EventLevel = 'info'): void { + if (!this.enabled || !this.configManager.shouldSample()) return; + + const event: ErrorEvent = { + type: 'error', + level, + message, + timestamp: now(), + breadcrumbs: [...this.breadcrumbs], + }; + + if (this.configManager.isIgnoredError(message)) return; + + this.queue.enqueue(event); + } + + captureEvent(event: Partial & { type: string }): void { + if (!this.enabled || !this.configManager.shouldSample()) return; + + const fullEvent = { + timestamp: now(), + level: 'info' as EventLevel, + ...event, + } as SentryEvent; + + this.queue.enqueue(fullEvent); + } + + private buildErrorEvent(error: Error | unknown): ErrorEvent { + const timestamp = now(); + + if (error instanceof Error) { + const frames = this.parseStackTrace(error.stack); + const fingerprint = computeFingerprint(error.name, error.message, frames); + + return { + type: 'error', + level: 'error', + message: error.message, + timestamp, + exception: { + type: error.name, + value: error.message, + stacktrace: { + frames: frames.slice(0, 5), + }, + }, + fingerprint, + }; + } + + const message = String(error); + return { + type: 'error', + level: 'error', + message, + timestamp, + }; + } + + 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); + } + + setTag(key: string, value: string): void { + this.configManager.setTag(key, value); + } + + setTags(tags: Record): void { + this.configManager.setTags(tags); + } + + setExtra(key: string, value: unknown): void { + this.configManager.setExtra(key, value); + } + + addBreadcrumb(breadcrumb: Omit): void { + const fullBreadcrumb: Breadcrumb = { + ...breadcrumb, + timestamp: now(), + }; + + this.breadcrumbs.push(fullBreadcrumb); + if (this.breadcrumbs.length > this.maxBreadcrumbs) { + this.breadcrumbs.shift(); + } + } + + async flush(): Promise { + await this.queue.flush(); + } + + disable(): void { + this.enabled = false; + } + + enable(): void { + this.enabled = true; + } + + use(plugin: unknown): void { + if (plugin && typeof plugin === 'object' && 'name' in plugin && 'setup' in plugin) { + this.pluginManager.add(plugin as never); + } + } + + destroy(): void { + this.queue.destroy(); + this.pluginManager.destroy(); + this.eventBus.destroy(); + this.breadcrumbs = []; + } +} + +export default Client; diff --git a/src/core/ConfigManager.ts b/src/core/ConfigManager.ts new file mode 100644 index 0000000..fd8a9c8 --- /dev/null +++ b/src/core/ConfigManager.ts @@ -0,0 +1,183 @@ +import type { LightConfig, SentryEvent, UserInfo } from '../types'; +import { getContexts, getContextId, getPageUrl, getReferrer } from '../utils/env'; + +const DEFAULT_CONFIG: Partial = { + enabled: true, + sampleRate: 1, + maxQueueSize: 100, + flushInterval: 5000, + maxRetries: 3, + retryDelay: 1000, + environment: 'production', + // 上下文分层策略:按事件级别决定堆栈深度和 breadcrumbs 数量 + 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 }, + }, +}; + +export class ConfigManager { + private config: LightConfig; + private contexts: ReturnType | null = null; + private contextId: string | null = null; + private contextReported: boolean = false; + + constructor(config: LightConfig) { + this.config = { ...DEFAULT_CONFIG, ...config } as LightConfig; + } + + markContextReported(): void { + this.contextReported = true; + } + + get(key: K): LightConfig[K] { + return this.config[key]; + } + + getAll(): LightConfig { + return { ...this.config }; + } + + set(key: K, value: LightConfig[K]): void { + this.config[key] = value; + } + + setUser(user: UserInfo | null): void { + this.config.user = user || undefined; + } + + setTags(tags: Record): void { + this.config.tags = { ...(this.config.tags || {}), ...tags }; + } + + setTag(key: string, value: string): void { + if (!this.config.tags) { + this.config.tags = {}; + } + this.config.tags[key] = value; + } + + setExtra(key: string, value: unknown): void { + if (!this.config.extra) { + this.config.extra = {}; + } + (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); + }); + } + + /** + * 获取编码后的环境信息(延迟初始化,只在首次上报时计算) + * 注意:env.ts 的 getContexts 已经返回编码后的数据 + */ + private getEncodedContext(): ReturnType { + if (!this.contexts) { + this.contexts = getContexts(); + } + return this.contexts; + } + + /** + * 将配置应用到事件 + * @param event 事件对象 + * @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 || {}) }; + } + + if (!event.request) { + event.request = { + url: getPageUrl(), + referrer: getReferrer(), + }; + } else if (!event.request.url) { + event.request.url = getPageUrl(); + } + + // context_id 总是带上,用于服务端关联环境信息 + if (!this.contextId) { + this.contextId = getContextId(); + } + event.context_id = this.contextId; + + // 应用上下文分层策略:按事件级别裁剪堆栈和 breadcrumbs + this.applyContextLevel(event); + + // 只有需要完整环境信息时才附加 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 = { + b: this.contexts.browser?.name, + bv: this.contexts.browser?.version, + os: this.contexts.os?.name, + osv: this.contexts.os?.version, + }; + } + + return event; + } + + /** + * 应用上下文分层策略:根据事件级别裁剪堆栈和 breadcrumbs + */ + private applyContextLevel(event: SentryEvent): void { + const contextLevel = this.config.contextLevel as Record; + if (!contextLevel) return; + + const level = event.level || 'error'; + 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; + } + + // 裁剪 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; + } + } +} diff --git a/src/core/EventBus.ts b/src/core/EventBus.ts new file mode 100644 index 0000000..0897ed6 --- /dev/null +++ b/src/core/EventBus.ts @@ -0,0 +1,47 @@ +type Handler = (...args: unknown[]) => void; + +export class EventBus { + private handlers: Map = new Map(); + + on(event: string, handler: Handler): void { + if (!this.handlers.has(event)) { + this.handlers.set(event, []); + } + this.handlers.get(event)!.push(handler); + } + + off(event: string, handler: Handler): void { + const eventHandlers = this.handlers.get(event); + if (eventHandlers) { + const index = eventHandlers.indexOf(handler); + if (index > -1) { + eventHandlers.splice(index, 1); + } + } + } + + emit(event: string, ...args: unknown[]): void { + const eventHandlers = this.handlers.get(event); + if (eventHandlers) { + for (const handler of eventHandlers) { + try { + handler(...args); + } catch (e) { + console.error('[LightSDK] EventBus handler error:', e); + } + } + } + } + + once(event: string, handler: Handler): void { + const wrapped = (...args: unknown[]) => { + this.off(event, wrapped); + handler(...args); + }; + this.on(event, wrapped); + } + + destroy(): void { + this.handlers.clear(); + } +} diff --git a/src/core/EventQueue.ts b/src/core/EventQueue.ts new file mode 100644 index 0000000..5d767f9 --- /dev/null +++ b/src/core/EventQueue.ts @@ -0,0 +1,247 @@ +import type { SentryEvent, ErrorEvent, CountEvent } from '../types'; +import { EventBus } from './EventBus'; +import { now } from '../utils/helper'; + +interface DedupeEntry { + count: number; + lastTime: number; + firstReported: boolean; +} + +export class EventQueue { + private queue: SentryEvent[] = []; + private maxSize: number; + private flushInterval: number; + private timer: ReturnType | null = null; + private eventBus: EventBus; + private flushCallback: (events: SentryEvent[]) => Promise; + private syncFlushCallback: (events: SentryEvent[]) => void; + private lastFlushTime: number = 0; + private dedupeMap: Map = new Map(); + private pendingCounts: Map = new Map(); + + private errorRateWindow: { [key: string]: number[] } = {}; + private paused: boolean = false; + private pauseTimer: ReturnType | null = null; + + constructor( + maxSize: number, + flushInterval: number, + eventBus: EventBus, + flushCallback: (events: SentryEvent[]) => Promise, + syncFlushCallback: (events: SentryEvent[]) => void + ) { + this.maxSize = maxSize; + this.flushInterval = flushInterval; + this.eventBus = eventBus; + this.flushCallback = flushCallback; + this.syncFlushCallback = syncFlushCallback; + } + + start(): void { + this.startTimer(); + this.setupVisibilityListener(); + } + + private startTimer(): void { + if (this.timer) return; + this.timer = setInterval(() => { + if (this.queue.length > 0 || this.pendingCounts.size > 0) { + this.flush(); + } + }, this.flushInterval); + } + + private setupVisibilityListener(): void { + if (typeof document !== 'undefined') { + document.addEventListener('visibilitychange', () => { + if (document.hidden && (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(); + } + }); + } + } + + private checkInfiniteLoop(fingerprint: string): boolean { + const currentTime = now(); + const windowStart = currentTime - 1000; + + if (!this.errorRateWindow[fingerprint]) { + this.errorRateWindow[fingerprint] = []; + } + + const window = this.errorRateWindow[fingerprint]; + window.push(currentTime); + + while (window.length > 0 && window[0] < windowStart) { + window.shift(); + } + + if (window.length > 10) { + if (!this.paused) { + console.warn('[LightSDK] Infinite loop detected, pausing SDK for 60s', { fingerprint, count: window.length }); + this.eventBus.emit('error', new Error('Infinite loop detected')); + this.paused = true; + + if (this.pauseTimer) { + clearTimeout(this.pauseTimer); + } + this.pauseTimer = setTimeout(() => { + this.paused = false; + this.errorRateWindow = {}; + console.info('[LightSDK] Resumed after infinite loop detection'); + }, 60000); + } + return true; + } + + return false; + } + + enqueue(event: SentryEvent): void { + if (this.paused) return; + + const fingerprint = this.getDedupeKey(event); + + if (fingerprint) { + if (this.checkInfiniteLoop(fingerprint)) { + return; + } + + const currentTime = now(); + const existing = this.dedupeMap.get(fingerprint); + + if (existing) { + if (currentTime - existing.lastTime < 60000) { + if (existing.count >= 3) { + if (!this.pendingCounts.has(fingerprint)) { + this.pendingCounts.set(fingerprint, { count: 1, ts_start: currentTime, ts_end: currentTime }); + } else { + const pending = this.pendingCounts.get(fingerprint)!; + pending.count++; + pending.ts_end = currentTime; + } + return; + } + existing.count++; + existing.lastTime = currentTime; + } else { + this.dedupeMap.set(fingerprint, { count: 1, lastTime: currentTime, firstReported: true }); + this.pendingCounts.delete(fingerprint); + } + } else { + this.dedupeMap.set(fingerprint, { count: 1, lastTime: currentTime, firstReported: false }); + } + } + + if (this.queue.length >= this.maxSize) { + this.flush(); + } + + this.queue.push(event); + this.eventBus.emit('event', event); + + if (this.queue.length >= this.maxSize) { + this.flush(); + } + } + + private buildCountEvents(): SentryEvent[] { + const countEvents: SentryEvent[] = []; + + for (const [fingerprint, data] of this.pendingCounts.entries()) { + // 构建聚合计数事件 + const event: CountEvent = { + type: 'count', + fingerprint, + count: data.count, + ts_start: data.ts_start, + ts_end: data.ts_end, + timestamp: now(), + level: 'info', + }; + countEvents.push(event); + } + + // 清空已上报的聚合计数 + this.pendingCounts.clear(); + return countEvents; + } + + private getDedupeKey(event: SentryEvent): string | null { + if (event.type === 'error') { + const errEvent = event as ErrorEvent; + return errEvent.fingerprint || errEvent.message; + } + return null; + } + + async flush(): Promise { + if (this.queue.length === 0 && this.pendingCounts.size === 0) return; + + const events = this.queue.splice(0, this.queue.length); + const countEvents = this.buildCountEvents(); + const allEvents = [...events, ...countEvents]; + + this.lastFlushTime = now(); + this.eventBus.emit('report', allEvents); + + try { + await this.flushCallback(allEvents); + this.eventBus.emit('reported', allEvents); + } catch (e) { + events.forEach(evt => this.queue.unshift(evt)); + throw e; + } + } + + flushSync(): void { + if (this.queue.length === 0 && this.pendingCounts.size === 0) return; + + const events = this.queue.splice(0, this.queue.length); + const countEvents = this.buildCountEvents(); + const allEvents = [...events, ...countEvents]; + + this.lastFlushTime = now(); + this.eventBus.emit('report', allEvents); + + try { + this.syncFlushCallback(allEvents); + this.eventBus.emit('reported', allEvents); + } catch (e) { + console.error('[LightSDK] Sync flush failed', e); + } + } + + size(): number { + return this.queue.length; + } + + destroy(): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + if (this.pauseTimer) { + clearTimeout(this.pauseTimer); + this.pauseTimer = null; + } + this.dedupeMap.clear(); + this.pendingCounts.clear(); + this.errorRateWindow = {}; + } +} diff --git a/src/core/PluginManager.ts b/src/core/PluginManager.ts new file mode 100644 index 0000000..24a9a0b --- /dev/null +++ b/src/core/PluginManager.ts @@ -0,0 +1,87 @@ +import type { LightPlugin, LightClient, SentryEvent } from '../types'; + +export class PluginManager { + private plugins: Map = new Map(); + private client: LightClient; + + constructor(client: LightClient) { + this.client = client; + } + + add(plugin: LightPlugin): void { + if (this.plugins.has(plugin.name)) { + return; + } + this.plugins.set(plugin.name, plugin); + try { + plugin.setup(this.client); + } catch (e) { + console.error(`[LightSDK] Plugin "${plugin.name}" setup error:`, e); + } + } + + remove(name: string): void { + const plugin = this.plugins.get(name); + if (plugin) { + if (plugin.destroy) { + try { + plugin.destroy(); + } catch (e) { + console.error(`[LightSDK] Plugin "${name}" destroy error:`, e); + } + } + this.plugins.delete(name); + } + } + + get(name: string): LightPlugin | undefined { + return this.plugins.get(name); + } + + has(name: string): boolean { + return this.plugins.has(name); + } + + applyBeforeReport(event: SentryEvent): SentryEvent | null { + let result: SentryEvent | null = event; + for (const plugin of this.plugins.values()) { + if (plugin.beforeReport) { + try { + const modified = plugin.beforeReport(result); + if (modified === null) { + return null; + } + result = modified; + } catch (e) { + console.error(`[LightSDK] Plugin "${plugin.name}" beforeReport error:`, e); + } + } + } + return result; + } + + applyAfterReport(event: SentryEvent): void { + for (const plugin of this.plugins.values()) { + if (plugin.afterReport) { + try { + plugin.afterReport(event); + } catch (e) { + console.error(`[LightSDK] Plugin "${plugin.name}" afterReport error:`, e); + } + } + } + } + + destroy(): void { + for (const plugin of this.plugins.values()) { + if (plugin.destroy) { + try { + plugin.destroy(); + } catch (e) { + console.error(`[LightSDK] Plugin "${plugin.name}" destroy error:`, e); + } + } + } + this.plugins.clear(); + } +} diff --git a/src/core/Reporter.ts b/src/core/Reporter.ts new file mode 100644 index 0000000..711311f --- /dev/null +++ b/src/core/Reporter.ts @@ -0,0 +1,336 @@ +import type { DSNInfo, SentryEvent, ErrorEvent } from '../types'; +import { getEnvelopeUrl } from '../utils/dsn'; +import { now } from '../utils/helper'; +import { encodeFields, FIELD_ENCODE_MAP } from '../utils/field-encoder'; + +export class Reporter { + private dsn: DSNInfo; + private maxRetries: number; + private retryDelay: number; + private useShortFields: boolean = true; // 是否使用短字段编码 + + constructor(dsn: DSNInfo, maxRetries: number, retryDelay: number) { + this.dsn = dsn; + this.maxRetries = maxRetries; + this.retryDelay = retryDelay; + } + + async report(events: SentryEvent[]): Promise { + if (events.length === 0) return; + + const envelope = this.buildEnvelope(events); + + let retries = 0; + while (retries <= this.maxRetries) { + try { + await this.send(envelope, false); + return; + } catch (e) { + const status = (e as { status?: number }).status; + if (status && status >= 400 && status < 500) { + throw e; + } + retries++; + if (retries > this.maxRetries) { + throw e; + } + await this.delay(this.retryDelay * Math.pow(2, retries - 1)); + } + } + } + + reportSync(events: SentryEvent[]): void { + if (events.length === 0) return; + + const envelope = this.buildEnvelope(events); + + try { + const success = this.sendSync(envelope); + if (success) return; + } catch { + // ignore sync errors + } + + this.sendViaImage(envelope); + } + + /** + * 构建 Sentry Envelope + * + * 优化: + * 1. 批量元数据共享 - 同批次共享 release/environment/user + * 2. 字段短编码 - 常用字段名使用短码 + * 3. 时间戳相对化 - 批次内后续事件用差值 + */ + private buildEnvelope(events: SentryEvent[]): string { + // 提取公共元数据(从第一个事件) + const sharedMeta = this.extractSharedMeta(events); + + // 构建 header(包含公共元数据) + const headerObj: Record = { + event_id: this.generateEventId(), + sent_at: new Date().toISOString(), + meta: sharedMeta, + }; + + // 如果启用短字段编码,在 header 中标记 + if (this.useShortFields) { + headerObj._sf = 1; // short fields flag + } + + // 时间戳相对化:第一个事件带基准时间,后续事件用差值 + const eventsWithRelativeTs = this.applyRelativeTimestamps(events, headerObj); + + const header = JSON.stringify(headerObj); + + const items: string[] = [header]; + + for (const event of eventsWithRelativeTs) { + // 移除已共享的字段,减少重复 + let strippedEvent = this.stripSharedFields(event, sharedMeta); + + // 短字段编码(如果启用) + if (this.useShortFields) { + strippedEvent = encodeFields(strippedEvent as Record) as SentryEvent; + } + + const itemPayload = JSON.stringify(strippedEvent); + const itemHeader = JSON.stringify({ + type: this.getEnvelopeType(event), + length: itemPayload.length, + }); + items.push(itemHeader, itemPayload); + } + + return items.join('\n'); + } + + /** + * 时间戳相对化 + * + * 批次内: + * - 第一个事件:完整时间戳 + * - 后续事件:相对于第一个事件的毫秒差值(用 _dts 字段表示) + * + * 这样可以将时间戳从 13 位数字减少到 2-4 位数字 + */ + private applyRelativeTimestamps(events: SentryEvent[], headerObj: Record): SentryEvent[] { + if (events.length <= 1) return events; + + const result: SentryEvent[] = []; + const baseTimestamp = events[0].timestamp ? new Date(events[0].timestamp).getTime() : Date.now(); + + // 在 header 中标记使用相对时间戳 + headerObj._rt = 1; // relative timestamp flag + headerObj._bt = baseTimestamp; // base timestamp + + for (let i = 0; i < events.length; i++) { + const event = { ...events[i] } as Record; + + if (i === 0) { + // 第一个事件保持完整时间戳 + result.push(event as SentryEvent); + } else { + // 后续事件用差值 + const eventTs = event.timestamp ? new Date(event.timestamp as string | number).getTime() : Date.now(); + const delta = eventTs - baseTimestamp; + + delete event.timestamp; + event._dts = delta; // delta timestamp + + // 同时处理 start_timestamp(transaction 事件) + if (event.start_timestamp) { + const startTs = new Date(event.start_timestamp as string | number).getTime(); + const startDelta = startTs - baseTimestamp; + delete event.start_timestamp; + event._dsts = startDelta; // delta start timestamp + } + + result.push(event as SentryEvent); + } + } + + return result; + } + + /** + * 提取批次内公共元数据 + */ + private extractSharedMeta(events: SentryEvent[]): Record { + if (events.length === 0) return {}; + + const firstEvent = events[0]; + const meta: Record = {}; + + // 提取 release(如果所有事件都相同) + if (firstEvent.release) { + const allSameRelease = events.every(e => e.release === firstEvent.release); + if (allSameRelease) { + meta.release = firstEvent.release; + } + } + + // 提取 environment(如果所有事件都相同) + if (firstEvent.environment) { + const allSameEnv = events.every(e => e.environment === firstEvent.environment); + if (allSameEnv) { + meta.environment = firstEvent.environment; + } + } + + // 提取 user(如果所有事件都相同) + if (firstEvent.user) { + const allSameUser = events.every(e => + JSON.stringify(e.user) === JSON.stringify(firstEvent.user) + ); + if (allSameUser) { + meta.user = firstEvent.user; + } + } + + // 提取 env(编码后的环境信息) + const firstEnv = (firstEvent as Record).env; + if (firstEnv) { + meta.env = firstEnv; + } + + return meta; + } + + /** + * 移除已共享的字段,减少重复 + */ + private stripSharedFields(event: SentryEvent, meta: Record): SentryEvent { + const stripped = { ...event }; + + // 移除已共享的字段 + if (meta.release && stripped.release === meta.release) { + delete stripped.release; + } + if (meta.environment && stripped.environment === meta.environment) { + delete stripped.environment; + } + if (meta.user && JSON.stringify(stripped.user) === JSON.stringify(meta.user)) { + delete stripped.user; + } + if (meta.env) { + delete (stripped as Record).env; + } + + return stripped; + } + + private getEnvelopeType(event: SentryEvent): string { + switch (event.type) { + case 'error': + return 'event'; + case 'performance': + return 'transaction'; + default: + return event.type; + } + } + + 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); + + if (navigator.sendBeacon) { + try { + const blob = new Blob([body], { type: 'application/x-sentry-envelope' }); + const success = navigator.sendBeacon(url, blob); + if (success) return; + } catch { + // sendBeacon failed, fall through + } + } + + if (typeof fetch === 'function') { + try { + const response = await fetch(url, { + method: 'POST', + body, + headers: { + 'Content-Type': 'application/x-sentry-envelope', + }, + keepalive: true, + }); + if (response.ok) return; + const err = new Error(`HTTP ${response.status}`) as Error & { status: number }; + err.status = response.status; + throw err; + } catch (e) { + throw e; + } + } + + if (typeof XMLHttpRequest !== 'undefined') { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, !isSync); + xhr.setRequestHeader('Content-Type', 'application/x-sentry-envelope'); + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + const err = new Error(`HTTP ${xhr.status}`) as Error & { status: number }; + err.status = xhr.status; + reject(err); + } + }; + xhr.onerror = () => reject(new Error('Network error')); + xhr.send(body); + }); + } + + throw new Error('No transport available'); + } + + private sendSync(body: string): boolean { + const url = getEnvelopeUrl(this.dsn); + + if (navigator.sendBeacon) { + try { + const blob = new Blob([body], { type: 'application/x-sentry-envelope' }); + return navigator.sendBeacon(url, blob); + } catch { + return false; + } + } + + if (typeof XMLHttpRequest !== 'undefined') { + try { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, false); + xhr.setRequestHeader('Content-Type', 'application/x-sentry-envelope'); + xhr.send(body); + return xhr.status >= 200 && xhr.status < 300; + } catch { + return false; + } + } + + return false; + } + + private sendViaImage(body: string): boolean { + try { + const url = getEnvelopeUrl(this.dsn); + const img = new Image(); + const encoded = encodeURIComponent(btoa(body)); + img.src = url + '&sentry_data=' + encoded.substring(0, 2000); + return true; + } catch { + return false; + } + } + + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ab42bf5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,148 @@ +import Client from './core/Client'; +import ErrorPlugin from './plugins/ErrorPlugin'; +import PerformancePlugin from './plugins/PerformancePlugin'; +import NetworkPlugin from './plugins/NetworkPlugin'; +import BehaviorPlugin from './plugins/BehaviorPlugin'; +import OfflinePlugin from './plugins/OfflinePlugin'; +import type { LightConfig, LightClient, SentryEvent, EventLevel, UserInfo, Breadcrumb, LightPlugin, ErrorEvent, PerformanceEvent, NetworkEvent, BehaviorEvent, DSNInfo } from './types'; + +let globalClient: LightClient | null = null; + +function init(config: LightConfig): LightClient { + const client = new Client(config); + + const defaultPlugins: LightPlugin[] = [ + new ErrorPlugin(), + new OfflinePlugin(), + ]; + + const pluginNames = config.plugins?.filter(p => typeof p === 'string') as string[] || []; + + if (pluginNames.includes('performance') || pluginNames.includes('all')) { + defaultPlugins.push(new PerformancePlugin()); + } + if (pluginNames.includes('network') || pluginNames.includes('all')) { + defaultPlugins.push(new NetworkPlugin()); + } + if (pluginNames.includes('behavior') || pluginNames.includes('all')) { + defaultPlugins.push(new BehaviorPlugin()); + } + + for (const plugin of defaultPlugins) { + (client as unknown as { use: (p: LightPlugin) => void }).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 as unknown as { init: () => void }).init(); + + globalClient = client; + return client; +} + +function getClient(): LightClient | null { + return globalClient; +} + +function captureException(error: Error | unknown): void { + globalClient?.captureException(error); +} + +function captureMessage(message: string, level?: EventLevel): void { + globalClient?.captureMessage(message, level); +} + +function captureEvent(event: Partial & { type: string }): void { + globalClient?.captureEvent(event); +} + +function setUser(user: UserInfo | null): void { + globalClient?.setUser(user); +} + +function setTag(key: string, value: string): void { + globalClient?.setTag(key, value); +} + +function setTags(tags: Record): void { + globalClient?.setTags(tags); +} + +function setExtra(key: string, value: unknown): void { + globalClient?.setExtra(key, value); +} + +function addBreadcrumb(breadcrumb: Omit): void { + globalClient?.addBreadcrumb(breadcrumb); +} + +async function flush(): Promise { + await globalClient?.flush(); +} + +function disable(): void { + globalClient?.disable(); +} + +function enable(): void { + globalClient?.enable(); +} + +export { + init, + getClient, + captureException, + captureMessage, + captureEvent, + setUser, + setTag, + setTags, + setExtra, + addBreadcrumb, + flush, + disable, + enable, + Client, + ErrorPlugin, + PerformancePlugin, + NetworkPlugin, + BehaviorPlugin, + OfflinePlugin, +}; + +export type { + LightConfig, + LightClient, + LightPlugin, + SentryEvent, + ErrorEvent, + PerformanceEvent, + NetworkEvent, + BehaviorEvent, + EventLevel, + UserInfo, + Breadcrumb, + DSNInfo, +}; + +export default { + init, + getClient, + captureException, + captureMessage, + captureEvent, + setUser, + setTag, + setTags, + setExtra, + addBreadcrumb, + flush, + disable, + enable, +}; diff --git a/src/plugins/BehaviorPlugin.ts b/src/plugins/BehaviorPlugin.ts new file mode 100644 index 0000000..7251be0 --- /dev/null +++ b/src/plugins/BehaviorPlugin.ts @@ -0,0 +1,290 @@ +import type { LightPlugin, LightClient, BehaviorEvent, LightConfig } from '../types'; +import { now, isObject } from '../utils/helper'; +import { getPageUrl } from '../utils/env'; + +interface BehaviorPluginConfig { + capturePV?: boolean; + captureClick?: boolean; + captureRoute?: boolean; + captureDuration?: boolean; + captureScroll?: boolean; + clickThrottle?: number; + scrollThrottle?: number; + sampleRate?: number; +} + +class BehaviorPlugin implements LightPlugin { + name = 'behavior'; + version = '1.0.0'; + + private client!: LightClient; + private config: BehaviorPluginConfig = { + capturePV: true, + captureClick: true, + captureRoute: true, + captureDuration: true, + captureScroll: true, + clickThrottle: 300, + scrollThrottle: 1000, + sampleRate: 0.1, + }; + + private lastClickTime: number = 0; + private lastScrollTime: number = 0; + private maxScrollDepth: number = 0; + private pageEnterTime: number = 0; + private scrollReported: boolean = false; + + setup(client: LightClient): void { + this.client = client; + this.loadConfig(); + + if (!this.shouldSample()) { + return; + } + + if (this.config.capturePV) { + this.trackPV(); + } + if (this.config.captureClick) { + this.trackClick(); + } + if (this.config.captureRoute) { + this.trackRoute(); + } + if (this.config.captureDuration) { + this.trackPageDuration(); + } + if (this.config.captureScroll) { + this.trackScroll(); + } + } + + private loadConfig(): void { + const clientConfig = this.client.config as LightConfig; + if (clientConfig && isObject(clientConfig.behavior)) { + this.config = { ...this.config, ...(clientConfig.behavior as object) }; + } + } + + private shouldSample(): boolean { + const rate = this.config.sampleRate ?? 0.1; + if (rate >= 1) return true; + if (rate <= 0) return false; + return Math.random() < rate; + } + + private getScrollDepth(): number { + if (typeof window === 'undefined' || typeof document === 'undefined') return 0; + + const scrollTop = window.scrollY || document.documentElement.scrollTop; + const scrollHeight = document.documentElement.scrollHeight; + const clientHeight = document.documentElement.clientHeight; + + if (scrollHeight <= clientHeight) return 100; + + return Math.min(100, Math.round((scrollTop / (scrollHeight - clientHeight)) * 100)); + } + + private trackPV(): void { + if (typeof window === 'undefined') return; + + const url = getPageUrl(); + const referrer = document.referrer; + + this.reportBehavior({ + sub_type: 'pv', + page_url: url, + referrer, + properties: { + title: document.title, + user_agent: navigator.userAgent, + screen_width: screen.width, + screen_height: screen.height, + viewport_width: window.innerWidth, + viewport_height: window.innerHeight, + language: navigator.language, + }, + }); + } + + private trackClick(): void { + if (typeof document === 'undefined') return; + + document.addEventListener('click', (e) => { + const currentTime = now(); + const throttle = this.config.clickThrottle ?? 300; + if (currentTime - this.lastClickTime < throttle) { + return; + } + this.lastClickTime = currentTime; + + const target = e.target as HTMLElement; + if (!target || !target.tagName) return; + + let selector = target.tagName.toLowerCase() || ''; + if (target.id) { + selector += `#${target.id}`; + } + if (target.className && typeof target.className === 'string') { + const classes = target.className.split(' ').filter(Boolean).slice(0, 3).join('.'); + if (classes) { + selector += `.${classes}`; + } + } + + const text = target.textContent?.trim().slice(0, 50); + + this.reportBehavior({ + sub_type: 'click', + page_url: getPageUrl(), + properties: { + selector, + text: text || undefined, + tag: target.tagName?.toLowerCase(), + x: e.clientX, + y: e.clientY, + }, + }); + }, true); + } + + private trackRoute(): void { + if (typeof window === 'undefined') return; + + let lastUrl = getPageUrl(); + + const checkRoute = () => { + const currentUrl = getPageUrl(); + if (currentUrl !== lastUrl) { + const from = lastUrl; + const to = currentUrl; + lastUrl = currentUrl; + + this.reportBehavior({ + sub_type: 'route', + page_url: to, + referrer: from, + properties: { + from, + to, + }, + }); + + this.pageEnterTime = now(); + this.maxScrollDepth = 0; + this.scrollReported = false; + } + }; + + const originalPushState = history.pushState; + const originalReplaceState = history.replaceState; + + history.pushState = function () { + const result = 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); + setTimeout(checkRoute, 0); + return result; + }; + + window.addEventListener('popstate', () => { + setTimeout(checkRoute, 0); + }); + + window.addEventListener('hashchange', () => { + setTimeout(checkRoute, 0); + }); + } + + private trackPageDuration(): void { + if (typeof window === 'undefined') return; + + this.pageEnterTime = now(); + + const sendDuration = () => { + const duration = now() - this.pageEnterTime; + if (duration > 1000) { + if (this.config.captureScroll && !this.scrollReported) { + this.reportScroll(); + } + + this.reportBehavior({ + sub_type: 'duration', + page_url: getPageUrl(), + properties: { + duration, + max_scroll_depth: this.maxScrollDepth, + }, + }); + } + }; + + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + sendDuration(); + } else { + this.pageEnterTime = now(); + } + }); + + window.addEventListener('beforeunload', sendDuration); + window.addEventListener('pagehide', sendDuration); + } + + private trackScroll(): void { + if (typeof window === 'undefined') return; + + const onScroll = () => { + const currentTime = now(); + const throttle = this.config.scrollThrottle ?? 1000; + if (currentTime - this.lastScrollTime < throttle) { + return; + } + this.lastScrollTime = currentTime; + + const depth = this.getScrollDepth(); + if (depth > this.maxScrollDepth) { + this.maxScrollDepth = depth; + } + }; + + window.addEventListener('scroll', onScroll, { passive: true }); + } + + private reportScroll(): void { + if (this.scrollReported) return; + this.scrollReported = true; + + this.reportBehavior({ + sub_type: 'scroll', + page_url: getPageUrl(), + properties: { + max_depth: this.maxScrollDepth, + }, + }); + } + + private reportBehavior(data: Omit): void { + const event: BehaviorEvent = { + type: 'behavior', + level: 'info', + timestamp: now(), + tags: { + sub_type: data.sub_type, + page_url: getPageUrl(), + }, + ...data, + }; + + this.client.captureEvent(event); + } + + destroy(): void {} +} + +export default BehaviorPlugin; diff --git a/src/plugins/ErrorPlugin.ts b/src/plugins/ErrorPlugin.ts new file mode 100644 index 0000000..3b7a787 --- /dev/null +++ b/src/plugins/ErrorPlugin.ts @@ -0,0 +1,262 @@ +import type { LightPlugin, LightClient, ErrorEvent, StackFrame, LightConfig } from '../types'; +import { now, isObject } from '../utils/helper'; +import { computeFingerprint } from '../utils/hash'; +import { shouldIgnoreUrl } from '../utils/env'; + +interface ErrorPluginConfig { + maxStackFrames?: number; + captureNodeModules?: boolean; + captureColumn?: boolean; + relativePathOnly?: boolean; + ignoreErrors?: (string | RegExp)[]; + ignoreUrls?: (string | RegExp)[]; +} + +class ErrorPlugin implements LightPlugin { + name = 'error'; + version = '1.0.0'; + + private client!: LightClient; + private config: ErrorPluginConfig = { + maxStackFrames: 5, + captureNodeModules: false, + captureColumn: true, + relativePathOnly: false, + }; + + setup(client: LightClient): void { + this.client = client; + this.loadConfig(); + this.setupGlobalError(); + this.setupUnhandledRejection(); + this.setupResourceError(); + } + + private loadConfig(): void { + const clientConfig = this.client.config as LightConfig; + if (clientConfig && isObject(clientConfig.error)) { + this.config = { ...this.config, ...(clientConfig.error as object) }; + } + if (clientConfig.ignoreErrors) { + this.config.ignoreErrors = [...(this.config.ignoreErrors || []), ...clientConfig.ignoreErrors]; + } + if (clientConfig.ignoreUrls) { + this.config.ignoreUrls = [...(this.config.ignoreUrls || []), ...clientConfig.ignoreUrls]; + } + } + + 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); + }); + } + + private shouldIgnoreScriptUrl(url: string): boolean { + return shouldIgnoreUrl(url, this.config.ignoreUrls); + } + + private setupGlobalError(): void { + if (typeof window === 'undefined') return; + + const originalOnError = window.onerror; + + window.onerror = (message, url, lineno, colno, error) => { + try { + if (originalOnError) { + originalOnError.call(window, message, url, lineno, colno, error); + } + + const errorEvent = this.buildErrorEvent( + error || (message as string), + url as string, + lineno as number, + colno as number + ); + + if (errorEvent) { + this.client.captureEvent(errorEvent); + } + } catch (e) { + console.error('[LightSDK] ErrorPlugin global error handler error:', e); + } + + return false; + }; + } + + private setupUnhandledRejection(): void { + if (typeof window === 'undefined') return; + + window.addEventListener('unhandledrejection', (event) => { + try { + const reason = event.reason; + const errorEvent = this.buildErrorEvent(reason); + + if (errorEvent) { + if (errorEvent.type === 'error') { + (errorEvent as ErrorEvent).tags = { + ...((errorEvent as ErrorEvent).tags || {}), + unhandled: 'true', + promise: 'true', + }; + } + + this.client.captureEvent(errorEvent); + } + } catch (e) { + console.error('[LightSDK] ErrorPlugin unhandledrejection handler error:', e); + } + }); + } + + private setupResourceError(): void { + if (typeof window === 'undefined') return; + + window.addEventListener('error', (event) => { + const target = event.target; + if (!target) return; + + const tagName = (target as HTMLElement).tagName?.toLowerCase(); + const src = (target as HTMLImageElement).src || (target as HTMLLinkElement).href; + + if (tagName && src && ['img', 'script', 'link', 'audio', 'video'].includes(tagName)) { + try { + this.client.captureEvent({ + type: 'error', + level: 'warning', + message: `Resource load failed: ${tagName} ${src}`, + timestamp: now(), + tags: { + resource_type: tagName, + resource_url: src, + }, + exception: { + type: 'ResourceError', + value: `Failed to load ${tagName} resource`, + }, + }); + } catch (e) { + console.error('[LightSDK] ErrorPlugin resource error handler error:', e); + } + } + }, true); + } + + private buildErrorEvent(error: Error | unknown, url?: string, lineno?: number, colno?: number): Partial & { type: string } | null { + const timestamp = now(); + + if (error instanceof Error) { + if (this.shouldIgnoreError(error.message)) return null; + + const frames = this.parseStackTrace(error.stack); + const fingerprint = computeFingerprint(error.name, error.message, frames); + + return { + type: 'error', + level: 'error', + message: error.message, + timestamp, + exception: { + type: error.name, + value: error.message, + stacktrace: { + frames: frames.slice(0, this.config.maxStackFrames || 5), + }, + }, + fingerprint, + }; + } + + const message = String(error); + if (this.shouldIgnoreError(message)) return null; + + const frames: StackFrame[] = []; + if (url) { + frames.push({ + filename: url, + lineno, + colno: this.config.captureColumn ? colno : undefined, + in_app: true, + }); + } + + return { + type: 'error', + level: 'error', + message, + timestamp, + exception: { + type: 'Error', + value: message, + stacktrace: { + frames, + }, + }, + }; + } + + 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, + }); + } + } + } + + return frames.reverse(); + } + + destroy(): void {} +} + +export default ErrorPlugin; diff --git a/src/plugins/NetworkPlugin.ts b/src/plugins/NetworkPlugin.ts new file mode 100644 index 0000000..fa35b9e --- /dev/null +++ b/src/plugins/NetworkPlugin.ts @@ -0,0 +1,255 @@ +import type { LightPlugin, LightClient, NetworkEvent, LightConfig } from '../types'; +import { now, isObject } from '../utils/helper'; +import { sanitizeUrl, shouldIgnoreUrl } from '../utils/env'; + +interface NetworkPluginConfig { + ignoreUrls?: (string | RegExp)[]; + captureSuccess?: boolean; + captureBody?: boolean; + captureRequestBody?: boolean; + captureResponseBody?: boolean; + captureRequestHeaders?: string[]; + captureResponseHeaders?: string[]; + errorSampleRate?: number; + successSampleRate?: number; + slowThreshold?: number; + slowSampleRate?: number; + ignoreStatusCodes?: number[]; +} + +class NetworkPlugin implements LightPlugin { + name = 'network'; + version = '1.0.0'; + + private client!: LightClient; + private config: NetworkPluginConfig = { + captureSuccess: false, + captureBody: false, + captureRequestBody: false, + captureResponseBody: false, + captureRequestHeaders: ['content-type', 'user-agent'], + captureResponseHeaders: ['content-type', 'content-length'], + errorSampleRate: 1.0, + successSampleRate: 0.01, + slowThreshold: 3000, + slowSampleRate: 1.0, + ignoreStatusCodes: [], + }; + + private originalFetch!: typeof fetch; + private originalXHROpen!: typeof XMLHttpRequest.prototype.open; + private originalXHRSend!: typeof XMLHttpRequest.prototype.send; + + setup(client: LightClient): void { + this.client = client; + this.loadConfig(); + this.patchFetch(); + this.patchXHR(); + } + + private loadConfig(): void { + const clientConfig = this.client.config as LightConfig; + if (clientConfig && isObject(clientConfig.network)) { + this.config = { ...this.config, ...(clientConfig.network as object) }; + } + if (clientConfig.ignoreUrls) { + this.config.ignoreUrls = [...(this.config.ignoreUrls || []), ...clientConfig.ignoreUrls]; + } + } + + private shouldIgnore(url: string): boolean { + return shouldIgnoreUrl(url, this.config.ignoreUrls || []); + } + + private shouldSample(isError: boolean, duration: number): boolean { + let rate = isError ? (this.config.errorSampleRate ?? 1.0) : (this.config.successSampleRate ?? 0.01); + + if (!isError && duration >= (this.config.slowThreshold ?? 3000)) { + rate = this.config.slowSampleRate ?? 1.0; + } + + if (rate >= 1) return true; + if (rate <= 0) return false; + return Math.random() < rate; + } + + private patchFetch(): void { + if (typeof fetch === 'undefined') return; + + this.originalFetch = window.fetch; + const self = this; + + window.fetch = function (input: RequestInfo | URL, init?: RequestInit): Promise { + const startTime = now(); + const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; + const method = init?.method || 'GET'; + + if (self.shouldIgnore(url)) { + return self.originalFetch.call(window, input, init); + } + + const sanitizedUrl = sanitizeUrl(url); + const promise = self.originalFetch.call(window, input, init); + + promise.then( + (response) => { + const duration = now() - startTime; + const isSuccess = response.ok; + const statusCode = response.status; + + if (self.config.ignoreStatusCodes?.includes(statusCode)) { + return response; + } + + if (!self.shouldSample(!isSuccess, duration)) { + return response; + } + + if (!isSuccess || self.config.captureSuccess) { + self.reportNetwork({ + sub_type: 'fetch', + method: method.toUpperCase(), + url: sanitizedUrl, + status_code: statusCode, + duration, + success: isSuccess, + error: !isSuccess ? `HTTP ${statusCode}` : undefined, + }); + } + + return response; + }, + (error) => { + const duration = now() - startTime; + + if (self.shouldSample(true, duration)) { + self.reportNetwork({ + sub_type: 'fetch', + method: method.toUpperCase(), + url: sanitizedUrl, + duration, + success: false, + error: error?.message || 'Network error', + }); + } + + throw error; + } + ); + + return promise; + }; + } + + private patchXHR(): void { + if (typeof XMLHttpRequest === 'undefined') return; + + this.originalXHROpen = XMLHttpRequest.prototype.open; + this.originalXHRSend = XMLHttpRequest.prototype.send; + const self = this; + + 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(); + return self.originalXHROpen.apply(this, arguments as unknown as Parameters); + }; + + XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) { + const startTime = now(); + const method = (this as { _lightMethod?: string })._lightMethod || 'GET'; + const url = (this as { _lightUrl?: string })._lightUrl || ''; + + if (self.shouldIgnore(url)) { + return self.originalXHRSend.apply(this, arguments as unknown as Parameters); + } + + const sanitizedUrl = sanitizeUrl(url); + let requestSize = 0; + if (body && typeof body === 'string') { + requestSize = body.length; + } + + const onLoadEnd = () => { + 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 (!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, + }); + + this.removeEventListener('loadend', onLoadEnd); + }; + + this.addEventListener('loadend', onLoadEnd); + + return self.originalXHRSend.apply(this, arguments as unknown as Parameters); + }; + } + + private reportNetwork(data: Omit): void { + const event: NetworkEvent = { + type: 'network', + level: data.success ? 'info' : 'error', + timestamp: now(), + tags: { + method: data.method, + sub_type: data.sub_type, + success: String(data.success), + }, + ...data, + }; + + this.client.captureEvent(event); + } + + destroy(): void { + if (this.originalFetch) { + window.fetch = this.originalFetch; + } + if (this.originalXHROpen) { + XMLHttpRequest.prototype.open = this.originalXHROpen; + } + if (this.originalXHRSend) { + XMLHttpRequest.prototype.send = this.originalXHRSend; + } + } +} + +export default NetworkPlugin; diff --git a/src/plugins/OfflinePlugin.ts b/src/plugins/OfflinePlugin.ts new file mode 100644 index 0000000..96a7748 --- /dev/null +++ b/src/plugins/OfflinePlugin.ts @@ -0,0 +1,255 @@ +import type { LightPlugin, LightClient, SentryEvent, LightConfig } from '../types'; +import { now } from '../utils/helper'; + +const DB_NAME = 'light-sentry-offline'; +const STORE_NAME = 'events'; +const MAX_EVENTS = 1000; + +interface OfflineEvent { + id: string; + event: SentryEvent; + timestamp: number; +} + +class OfflinePlugin implements LightPlugin { + name = 'offline'; + version = '1.0.0'; + + private client!: LightClient; + private config: { maxEvents?: number } = { maxEvents: MAX_EVENTS }; + private db: IDBDatabase | null = null; + private isOnline: boolean = true; + private isSyncing: boolean = false; + private pendingEvents: SentryEvent[] = []; + + setup(client: LightClient): void { + this.client = client; + this.loadConfig(); + this.initDatabase(); + this.setupNetworkListeners(); + } + + private loadConfig(): void { + const clientConfig = this.client.config as LightConfig; + if (clientConfig && typeof clientConfig.offline === 'object') { + this.config = { ...this.config, ...(clientConfig.offline as object) }; + } + } + + private async initDatabase(): Promise { + if (typeof indexedDB === 'undefined') { + console.warn('[LightSDK] OfflinePlugin: IndexedDB not available'); + return; + } + + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, 1); + + request.onerror = () => { + console.warn('[LightSDK] OfflinePlugin: Failed to open IndexedDB'); + reject(request.error); + }; + + request.onsuccess = () => { + this.db = request.result; + resolve(); + }; + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + store.createIndex('timestamp', 'timestamp', { unique: false }); + } + }; + }); + } + + private setupNetworkListeners(): void { + if (typeof window === 'undefined') return; + + this.isOnline = navigator.onLine; + + window.addEventListener('online', () => { + console.info('[LightSDK] OfflinePlugin: Network online'); + this.isOnline = true; + this.syncPendingEvents(); + }); + + window.addEventListener('offline', () => { + console.info('[LightSDK] OfflinePlugin: Network offline'); + this.isOnline = false; + }); + } + + private generateId(): string { + return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; + } + + private async saveToIndexedDB(event: SentryEvent): Promise { + if (!this.db) return; + + 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); + }; + }); + } + + private async deleteOldestEvents(count: number): Promise { + if (!this.db || count <= 0) return; + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const index = store.index('timestamp'); + + const cursorRequest = index.openCursor(); + let deleted = 0; + + cursorRequest.onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result; + if (cursor && deleted < count) { + cursor.delete(); + deleted++; + cursor.continue(); + } else { + resolve(); + } + }; + + cursorRequest.onerror = () => reject(cursorRequest.error); + }); + } + + private async getAllStoredEvents(): Promise { + if (!this.db) return []; + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const index = store.index('timestamp'); + const request = index.getAll(); + + request.onsuccess = () => { + const events = request.result || []; + // 按时间排序 + events.sort((a, b) => a.timestamp - b.timestamp); + resolve(events); + }; + + request.onerror = () => reject(request.error); + }); + } + + private async clearStoredEvents(): Promise { + if (!this.db) return; + + return new Promise((resolve, reject) => { + const transaction = this.db!.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const request = store.clear(); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + async syncPendingEvents(): Promise { + if (!this.isOnline || this.isSyncing) return; + + this.isSyncing = true; + console.info('[LightSDK] OfflinePlugin: Syncing pending events'); + + try { + const storedEvents = await this.getAllStoredEvents(); + + if (storedEvents.length === 0) { + console.info('[LightSDK] OfflinePlugin: No pending events to sync'); + this.isSyncing = false; + return; + } + + // 分批上报,每次最多 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); + // 继续同步其他批次 + } + } + + // 清空已同步的事件 + await this.clearStoredEvents(); + console.info('[LightSDK] OfflinePlugin: All pending events synced'); + } catch (error) { + console.error('[LightSDK] OfflinePlugin: Sync failed', error); + } finally { + this.isSyncing = false; + } + } + + 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 表示不上报(因为已经缓存了) + return null; + } + + // 在线时,标记为非离线事件 + const onlineEvent = { ...event }; + delete (onlineEvent as Record).offline; + return onlineEvent as SentryEvent; + } + + destroy(): void { + // 清理资源 + if (this.db) { + this.db.close(); + this.db = null; + } + } +} + +export default OfflinePlugin; diff --git a/src/plugins/PerformancePlugin.ts b/src/plugins/PerformancePlugin.ts new file mode 100644 index 0000000..209640f --- /dev/null +++ b/src/plugins/PerformancePlugin.ts @@ -0,0 +1,303 @@ +import type { LightPlugin, LightClient, PerformanceEvent, LightConfig } from '../types'; +import { now, isObject } from '../utils/helper'; + +interface PerformancePluginConfig { + sampleRate?: number; + captureLongTasks?: boolean; + captureResources?: boolean; + resourceSampleRate?: number; + longTaskSampleRate?: number; + captureNavigation?: boolean; + captureFP?: boolean; +} + +class PerformancePlugin implements LightPlugin { + name = 'performance'; + version = '1.0.0'; + + private client!: LightClient; + private config: PerformancePluginConfig = { + sampleRate: 0.1, + captureLongTasks: true, + captureResources: false, + resourceSampleRate: 0.01, + longTaskSampleRate: 0.05, + captureNavigation: true, + captureFP: true, + }; + + setup(client: LightClient): void { + this.client = client; + this.loadConfig(); + + if (!this.shouldSample('default')) { + return; + } + + this.observeWebVitals(); + this.observeLongTasks(); + this.observeNavigation(); + this.observeResources(); + } + + private loadConfig(): void { + const clientConfig = this.client.config as LightConfig; + if (clientConfig && isObject(clientConfig.performance)) { + this.config = { ...this.config, ...(clientConfig.performance as object) }; + } + } + + private shouldSample(type: 'default' | 'resource' | 'longtask' = '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; + } + if (rate >= 1) return true; + if (rate <= 0) return false; + return Math.random() < rate; + } + + private observeWebVitals(): void { + if (typeof PerformanceObserver === 'undefined') return; + + if (this.config.captureFP) { + this.observeFP(); + } + this.observeLCP(); + this.observeFID(); + this.observeCLS(); + this.observeFCP(); + this.observeTTFB(); + } + + private observeFP(): void { + try { + const po = new PerformanceObserver((entryList) => { + const entries = entryList.getEntriesByName('first-paint'); + if (entries.length > 0) { + this.reportMetric('FP', entries[0].startTime, 'ms'); + } + }); + po.observe({ type: 'paint', buffered: true }); + } catch { + // FP not supported + } + } + + private observeLCP(): void { + try { + const po = new PerformanceObserver((entryList) => { + const entries = entryList.getEntries(); + 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'); + } + }); + po.observe({ type: 'largest-contentful-paint', buffered: true }); + } catch { + // LCP not supported + } + } + + private observeFID(): void { + try { + const po = new PerformanceObserver((entryList) => { + const entries = entryList.getEntries() as PerformanceEventTiming[]; + if (entries.length > 0) { + const firstEntry = entries[0]; + const value = firstEntry.processingStart - firstEntry.startTime; + this.reportMetric('FID', value, 'ms'); + } + }); + po.observe({ type: 'first-input', buffered: true }); + } catch { + // FID not supported + } + } + + private observeCLS(): void { + try { + let clsValue = 0; + let sessionValue = 0; + let sessionEntries: PerformanceEntry[] = []; + + const po = new PerformanceObserver((entryList) => { + const entries = entryList.getEntries() as PerformanceEntry & { hadRecentInput?: boolean; value?: number }[]; + for (const entry of entries) { + if (!entry.hadRecentInput) { + const firstSessionEntry = sessionEntries[0]; + const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; + + if ( + sessionValue && + entry.startTime - (lastSessionEntry as { endTime?: number }).endTime! < 1000 && + entry.startTime - firstSessionEntry.startTime < 5000 + ) { + sessionValue += entry.value || 0; + sessionEntries.push(entry as PerformanceEntry); + } else { + sessionValue = entry.value || 0; + sessionEntries = [entry as PerformanceEntry]; + } + + if (sessionValue > clsValue) { + clsValue = sessionValue; + this.reportMetric('CLS', clsValue, ''); + } + } + } + }); + po.observe({ type: 'layout-shift', buffered: true }); + } catch { + // CLS not supported + } + } + + private observeFCP(): void { + try { + const po = new PerformanceObserver((entryList) => { + const entries = entryList.getEntriesByName('first-contentful-paint'); + if (entries.length > 0) { + this.reportMetric('FCP', entries[0].startTime, 'ms'); + } + }); + po.observe({ type: 'paint', buffered: true }); + } catch { + // FCP not supported + } + } + + private observeTTFB(): void { + try { + const po = new PerformanceObserver((entryList) => { + const navEntries = entryList.getEntriesByType('navigation') as PerformanceNavigationTiming[]; + if (navEntries.length > 0) { + const navEntry = navEntries[0]; + const ttfb = navEntry.responseStart - navEntry.requestStart; + this.reportMetric('TTFB', Math.max(0, ttfb), 'ms'); + } + }); + po.observe({ type: 'navigation', buffered: true }); + } catch { + // Navigation timing not supported + } + } + + private observeLongTasks(): void { + if (!this.config.captureLongTasks) return; + + try { + const po = new PerformanceObserver((entryList) => { + const entries = entryList.getEntries(); + for (const entry of entries) { + if (this.shouldSample('longtask')) { + this.reportMetric('longtask', entry.duration, 'ms'); + } + } + }); + po.observe({ type: 'longtask', buffered: true }); + } catch { + // Long tasks not supported + } + } + + private observeNavigation(): void { + if (!this.config.captureNavigation) return; + if (typeof performance === 'undefined') return; + + window.addEventListener('load', () => { + setTimeout(() => { + const timing = performance.timing; + if (!timing) return; + + const navigationStart = timing.navigationStart; + const metrics: Record = { + dom_ready: timing.domContentLoadedEventEnd - navigationStart, + load_time: timing.loadEventEnd - navigationStart, + dns: timing.domainLookupEnd - timing.domainLookupStart, + tcp: timing.connectEnd - timing.connectStart, + ssl: timing.secureConnectionStart > 0 ? timing.connectEnd - timing.secureConnectionStart : 0, + ttfb: timing.responseStart - timing.requestStart, + download: timing.responseEnd - timing.responseStart, + dom_parse: timing.domInteractive - timing.responseEnd, + }; + + for (const [name, value] of Object.entries(metrics)) { + if (value > 0) { + this.reportMetric(name, value, 'ms'); + } + } + }, 0); + }); + } + + private observeResources(): void { + if (!this.config.captureResources) return; + if (typeof performance === 'undefined') return; + + 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, + }); + } + } + }); + po.observe({ type: 'resource', buffered: true }); + } catch { + // Resource timing not supported + } + } + + private getRating(name: string, value: number): 'good' | 'needs-improvement' | 'poor' { + const thresholds: Record = { + LCP: { good: 2500, poor: 4000 }, + FCP: { good: 1800, poor: 3000 }, + FP: { good: 1800, poor: 3000 }, + FID: { good: 100, poor: 300 }, + CLS: { good: 0.1, poor: 0.25 }, + TTFB: { good: 800, poor: 1800 }, + }; + + const threshold = thresholds[name]; + if (!threshold) return 'good'; + + if (value <= threshold.good) return 'good'; + if (value <= threshold.poor) return 'needs-improvement'; + return 'poor'; + } + + private reportMetric(name: string, value: number, unit: string, extraTags: Record = {}): void { + const rating = this.getRating(name, value); + + const event: PerformanceEvent = { + type: 'performance', + level: 'info', + metric: name, + value: Math.round(value * 100) / 100, + unit, + rating, + timestamp: now(), + tags: { + metric: name, + rating, + ...extraTags, + }, + }; + + this.client.captureEvent(event); + } + + destroy(): void {} +} + +export default PerformancePlugin; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..4647180 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,179 @@ +export type EventLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug'; + +export interface UserInfo { + id?: string; + username?: string; + email?: string; + [key: string]: unknown; +} + +export interface StackFrame { + filename: string; + function?: string; + lineno?: number; + colno?: number; + in_app?: boolean; +} + +export interface ExceptionInfo { + type: string; + value: string; + stacktrace?: { + frames: StackFrame[]; + }; +} + +export interface RequestInfo { + url: string; + method?: string; + headers?: Record; + referrer?: string; +} + +export interface BrowserContext { + name: string; + version?: string; +} + +export interface OSContext { + name: string; + version?: string; +} + +export interface DeviceContext { + family?: string; + model?: string; + brand?: string; + type?: string; +} + +export interface Contexts { + browser?: BrowserContext; + os?: OSContext; + device?: DeviceContext; +} + +export interface Breadcrumb { + type: string; + message: string; + timestamp: number; + category?: string; + data?: Record; + level?: EventLevel; +} + +export interface BaseEvent { + type: string; + level: EventLevel; + timestamp: number; + release?: string; + environment?: string; + user?: UserInfo; + tags?: Record; + extra?: Record; + breadcrumbs?: Breadcrumb[]; + request?: RequestInfo; + contexts?: Contexts; + context_id?: string; +} + +export interface ErrorEvent extends BaseEvent { + type: 'error'; + message: string; + exception?: ExceptionInfo; + fingerprint?: string; + title?: string; +} + +export interface PerformanceEvent extends BaseEvent { + type: 'performance'; + metric: string; + value: number; + unit: string; + rating?: 'good' | 'needs-improvement' | 'poor'; +} + +export interface NetworkEvent extends BaseEvent { + type: 'network'; + sub_type: 'fetch' | 'xhr'; + method: string; + url: string; + status_code?: number; + duration?: number; + request_size?: number; + response_size?: number; + success: boolean; + error?: string; +} + +export interface BehaviorEvent extends BaseEvent { + type: 'behavior'; + sub_type: 'pv' | 'click' | 'route' | 'scroll' | 'duration'; + page_url: string; + referrer?: string; + properties?: Record; +} + +export interface CountEvent extends BaseEvent { + type: 'count'; + fingerprint: string; + count: number; + ts_start: number; + ts_end: number; +} + +export type SentryEvent = ErrorEvent | PerformanceEvent | NetworkEvent | BehaviorEvent | CountEvent; + +export interface DSNInfo { + protocol: string; + publicKey: string; + host: string; + projectId: string; +} + +export interface LightConfig { + dsn: string; + release?: string; + environment?: string; + enabled?: boolean; + sampleRate?: number; + maxQueueSize?: number; + flushInterval?: number; + maxRetries?: number; + retryDelay?: number; + ignoreErrors?: (string | RegExp)[]; + ignoreUrls?: (string | RegExp)[]; + includePaths?: (string | RegExp)[]; + beforeSend?: (event: SentryEvent) => SentryEvent | null; + user?: UserInfo; + plugins?: (LightPlugin | string)[]; + [pluginName: string]: unknown; +} + +export interface LightPlugin { + name: string; + version: string; + setup(client: LightClient): void; + destroy?(): void; + beforeReport?(event: SentryEvent): SentryEvent | null; + afterReport?(event: SentryEvent): void; +} + +export interface LightClient { + config: LightConfig; + dsn: DSNInfo; + on(event: string, handler: (...args: unknown[]) => void): void; + off(event: string, handler: (...args: unknown[]) => void): void; + emit(event: string, ...args: unknown[]): void; + captureException(error: Error | unknown): void; + captureMessage(message: string, level?: EventLevel): void; + captureEvent(event: Partial & { type: string }): void; + setUser(user: UserInfo | null): void; + setTag(key: string, value: string): void; + setTags(tags: Record): void; + setExtra(key: string, value: unknown): void; + addBreadcrumb(breadcrumb: Omit): void; + flush(): Promise; + disable(): void; + enable(): void; +} diff --git a/src/utils/dsn.ts b/src/utils/dsn.ts new file mode 100644 index 0000000..acffe82 --- /dev/null +++ b/src/utils/dsn.ts @@ -0,0 +1,29 @@ +import type { DSNInfo } from '../types'; + +export function parseDSN(dsn: string): DSNInfo { + try { + const match = dsn.match(/^(https?):\/\/([^@]+)@([^/]+)\/(.+)$/); + if (!match) { + throw new Error('Invalid DSN format'); + } + + const [, protocol, publicKey, host, projectId] = match; + + return { + protocol, + publicKey, + host, + projectId, + }; + } catch (e) { + throw new Error(`Failed to parse DSN: ${(e as Error).message}`); + } +} + +export function getEnvelopeUrl(dsn: DSNInfo): string { + return `${dsn.protocol}://${dsn.host}/${dsn.projectId}/envelope/`; +} + +export function getStoreUrl(dsn: DSNInfo): string { + return `${dsn.protocol}://${dsn.host}/${dsn.projectId}/store/`; +} diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 0000000..6deb008 --- /dev/null +++ b/src/utils/env.ts @@ -0,0 +1,269 @@ +import { md5 } from './hash'; + +export interface BrowserInfo { + name: string; + version: string; + major: string; +} + +export interface OSInfo { + name: string; + version: string; +} + +export interface DeviceInfo { + type: 'desktop' | 'mobile' | 'tablet'; + vendor: string; + model: string; +} + +export interface EnvInfo { + browser: BrowserInfo; + os: OSInfo; + device: DeviceInfo; + url: string; + referrer: string; + title: string; + language: string; + user_agent: string; + screen_width: number; + screen_height: number; + viewport_width: number; + viewport_height: number; +} + +function detectBrowser(ua: string): BrowserInfo { + const browsers: { name: string; pattern: RegExp; versionIndex?: number }[] = [ + { 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 (const b of browsers) { + const match = ua.match(b.pattern); + if (match) { + const version = match[1] || ''; + const major = version.split('.')[0] || ''; + return { name: b.name, version, major }; + } + } + + return { name: 'Unknown', version: '', major: '' }; +} + +function detectOS(ua: string): OSInfo { + const oss: { name: string; pattern: RegExp; versionIndex?: number }[] = [ + { 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 (const o of oss) { + const match = ua.match(o.pattern); + if (match) { + let version = match[1] || ''; + if (version) { + version = version.replace(/_/g, '.'); + } + return { name: o.name, version }; + } + } + + return { name: 'Unknown', version: '' }; +} + +function detectDevice(ua: string): DeviceInfo { + const isMobile = /Mobile|Android|iPhone|iPod|BlackBerry|Windows Phone|Opera Mini/i.test(ua); + const isTablet = /Tablet|iPad|PlayBook|Kindle|Silk/i.test(ua); + + let type: 'desktop' | 'mobile' | 'tablet' = 'desktop'; + if (isTablet) { + type = 'tablet'; + } else if (isMobile) { + type = 'mobile'; + } + + let vendor = ''; + let model = ''; + + const iphoneMatch = ua.match(/iPhone/i); + const ipadMatch = ua.match(/iPad/i); + const samsungMatch = ua.match(/SM-([A-Z0-9]+)/i); + const pixelMatch = ua.match(/Pixel ([0-9A-Z]+)/i); + const huaweiMatch = ua.match(/HUAWEI ([A-Z0-9]+)/i); + + if (iphoneMatch) { + vendor = 'Apple'; + model = 'iPhone'; + } else if (ipadMatch) { + vendor = 'Apple'; + model = 'iPad'; + } else if (samsungMatch) { + vendor = 'Samsung'; + model = samsungMatch[1]; + } else if (pixelMatch) { + vendor = 'Google'; + model = `Pixel ${pixelMatch[1]}`; + } else if (huaweiMatch) { + vendor = 'Huawei'; + model = huaweiMatch[1]; + } + + return { type, vendor, model }; +} + +export function getEnvInfo(): EnvInfo { + const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''; + const browser = detectBrowser(ua); + const os = detectOS(ua); + const device = detectDevice(ua); + + return { + browser, + os, + device, + 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: ua, + 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, + }; +} + +export function getPageUrl(): string { + return typeof location !== 'undefined' ? location.href : ''; +} + +export function getReferrer(): string { + return typeof document !== 'undefined' ? document.referrer : ''; +} + +export function sanitizeUrl(url: string): string { + if (!url) return url; + + try { + const urlObj = new URL(url); + const sensitiveParams = [ + 'password', 'passwd', 'pwd', + 'token', 'access_token', 'refresh_token', + 'api_key', 'apikey', 'key', + 'secret', 'private_key', + 'code', 'auth', + 'id_token', 'idToken', + 'session', 'session_id', + ]; + + const params = urlObj.searchParams; + let modified = false; + + for (const key of Array.from(params.keys())) { + const lowerKey = key.toLowerCase().replace(/[-_]/g, '_'); + if (sensitiveParams.some(s => lowerKey.includes(s))) { + params.set(key, '[Filtered]'); + modified = true; + } + } + + if (modified) { + return urlObj.toString(); + } + return url; + } catch { + return url; + } +} + +export function shouldIgnoreUrl(url: string, ignoreUrls: (string | RegExp)[] = []): boolean { + if (!ignoreUrls || ignoreUrls.length === 0) return false; + + return ignoreUrls.some(pattern => { + if (typeof pattern === 'string') { + if (pattern.includes('*')) { + // 移除开头的 ^ 和结尾的 $,允许匹配 URL 的任意部分 + const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); + const regex = new RegExp(escaped); + return regex.test(url); + } + return url.includes(pattern); + } + return pattern.test(url); + }); +} + +const BROWSER_CODE_MAP: Record = { + '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', +}; + +const OS_CODE_MAP: Record = { + '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', +}; + +export function getContexts() { + const env = getEnvInfo(); + const browserCode = BROWSER_CODE_MAP[env.browser.name] || env.browser.name.toLowerCase(); + const osCode = OS_CODE_MAP[env.os.name] || env.os.name.toLowerCase(); + + return { + browser: { + name: browserCode, + version: env.browser.major, + }, + os: { + name: osCode, + version: env.os.version.split('.')[0], + }, + device: { + type: env.device.type, + brand: env.device.vendor || undefined, + model: env.device.model || undefined, + }, + }; +} + +export function getContextId(): string { + const env = getEnvInfo(); + const key = `${env.browser.name}|${env.browser.major}|${env.os.name}|${env.os.version.split('.')[0]}|${env.device.type}|${env.device.vendor}|${env.device.model}|${env.language}`; + return md5(key).substring(0, 16); +} diff --git a/src/utils/field-encoder.ts b/src/utils/field-encoder.ts new file mode 100644 index 0000000..2b28b5f --- /dev/null +++ b/src/utils/field-encoder.ts @@ -0,0 +1,88 @@ +/** + * 字段短编码映射 + * + * 将常用字段名编码为 2-3 个字符的短码,减少上报体积 + * + * 编码前:{ "type": "error", "level": "error", "message": "..." } + * 编码后:{ "t": "e", "l": "e", "m": "..." } + * + * 注意:只对最外层高频字段编码,不对嵌套对象深度编码 + * 避免复杂嵌套带来的解析问题和兼容性风险 + */ + +// SDK 端:字段名编码映射(只包含最外层高频字段) +export const FIELD_ENCODE_MAP: Record = { + // 基础字段 + 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 + transaction: 'tx', + duration: 'd', + spans: 'sp', + + // 额外信息 + extra: 'xt', +}; + +// 服务端:字段名解码映射(反向) +export const FIELD_DECODE_MAP: Record = Object.fromEntries( + Object.entries(FIELD_ENCODE_MAP).map(([k, v]) => [v, k]) +); + +/** + * 编码事件字段名(SDK 端使用) + * 只对最外层字段编码,避免深度编码的复杂性 + */ +export function encodeFields(obj: Record): Record { + if (!obj || typeof obj !== 'object') return obj; + + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + const shortKey = FIELD_ENCODE_MAP[key] || key; + result[shortKey] = value; + } + return result; +} + +/** + * 解码事件字段名(服务端使用) + * 只对最外层字段解码 + */ +export function decodeFields(obj: Record): Record { + if (!obj || typeof obj !== 'object') return obj; + + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + const fullKey = FIELD_DECODE_MAP[key] || key; + result[fullKey] = value; + } + return result; +} diff --git a/src/utils/hash.ts b/src/utils/hash.ts new file mode 100644 index 0000000..246b45b --- /dev/null +++ b/src/utils/hash.ts @@ -0,0 +1,27 @@ +export function hashString(str: string): number { + let hash = 5381; + let i = str.length; + while (i) { + hash = (hash * 33) ^ str.charCodeAt(--i); + } + return hash >>> 0; +} + +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'); +} + +export function computeFingerprint(type: string, message: string, frames: { filename: string; lineno?: number }[] = []): string { + const keyFrames = frames.slice(0, 3).map(f => `${f.filename}:${f.lineno || 0}`).join('|'); + const normalizedMessage = message.replace(/['"]?\d+['"]?/g, '').replace(/\s+/g, ' ').trim(); + return md5(`${type}:${normalizedMessage}:${keyFrames}`); +} diff --git a/src/utils/helper.ts b/src/utils/helper.ts new file mode 100644 index 0000000..05ed89b --- /dev/null +++ b/src/utils/helper.ts @@ -0,0 +1,42 @@ +export function generateEventId(): string { + return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, () => { + return ((Math.random() * 16) | 0).toString(16); + }); +} + +export function now(): number { + return Date.now(); +} + +export function uuid(): string { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID().replace(/-/g, ''); + } + return generateEventId(); +} + +export function isFunction(fn: unknown): fn is (...args: unknown[]) => unknown { + return typeof fn === 'function'; +} + +export function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +export function isObject(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +export function safeGet(fn: () => T, defaultValue: T): T { + try { + const result = fn(); + return result === undefined ? defaultValue : result; + } catch { + return defaultValue; + } +} + +export function truncate(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return str.slice(0, maxLen) + '...'; +} diff --git a/tests/ConfigManager.test.ts b/tests/ConfigManager.test.ts new file mode 100644 index 0000000..aad95e9 --- /dev/null +++ b/tests/ConfigManager.test.ts @@ -0,0 +1,248 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ConfigManager } from '../src/core/ConfigManager'; +import type { LightConfig, SentryEvent, ErrorEvent } from '../src/types'; + +// Mock the env module +vi.mock('../src/utils/env', () => ({ + getContexts: vi.fn(() => ({ + browser: { name: 'c', version: '120' }, + os: { name: 'm', version: '14' }, + device: { type: 'desktop' }, + })), + getContextId: vi.fn(() => 'test_context_id'), + getPageUrl: vi.fn(() => 'https://example.com/page'), + getReferrer: vi.fn(() => 'https://google.com'), +})); + +describe('ConfigManager', () => { + let configManager: ConfigManager; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('constructor', () => { + it('should create ConfigManager with valid config', () => { + const config: LightConfig = { + dsn: 'https://proj_abc123@log.example.com/1001', + release: '1.0.0', + environment: 'production', + }; + configManager = new ConfigManager(config); + expect(configManager).toBeDefined(); + }); + + it('should use default values for optional config', () => { + const config: LightConfig = { + dsn: 'https://proj_abc123@log.example.com/1001', + }; + configManager = new ConfigManager(config); + expect(configManager.get('enabled')).toBe(true); + expect(configManager.get('sampleRate')).toBe(1); + expect(configManager.get('environment')).toBe('production'); + }); + }); + + describe('get/set methods', () => { + beforeEach(() => { + configManager = new ConfigManager({ + dsn: 'https://proj_abc123@log.example.com/1001', + }); + }); + + it('should get configured value', () => { + expect(configManager.get('environment')).toBe('production'); + }); + + it('should set and get value', () => { + configManager.set('environment', 'development'); + expect(configManager.get('environment')).toBe('development'); + }); + + it('should set user info', () => { + configManager.setUser({ id: '123', username: 'test' }); + const config = configManager.getAll(); + expect(config.user?.id).toBe('123'); + }); + + it('should set single tag', () => { + configManager.setTag('page', 'home'); + const config = configManager.getAll(); + expect(config.tags?.page).toBe('home'); + }); + + it('should set multiple tags', () => { + configManager.setTags({ page: 'home', version: '2.0' }); + const config = configManager.getAll(); + expect(config.tags?.page).toBe('home'); + expect(config.tags?.version).toBe('2.0'); + }); + + it('should set extra data', () => { + configManager.setExtra('order_id', '12345'); + const config = configManager.getAll(); + expect(config.extra?.order_id).toBe('12345'); + }); + }); + + describe('shouldSample', () => { + it('should return true when sampleRate is 1', () => { + configManager = new ConfigManager({ + dsn: 'https://proj_abc123@log.example.com/1001', + sampleRate: 1, + }); + expect(configManager.shouldSample()).toBe(true); + }); + + it('should return false when sampleRate is 0', () => { + configManager = new ConfigManager({ + dsn: 'https://proj_abc123@log.example.com/1001', + sampleRate: 0, + }); + expect(configManager.shouldSample()).toBe(false); + }); + }); + + describe('isIgnoredError', () => { + beforeEach(() => { + configManager = new ConfigManager({ + dsn: 'https://proj_abc123@log.example.com/1001', + ignoreErrors: [/^Script error/i, 'ResizeObserver'], + }); + }); + + it('should ignore matching pattern', () => { + expect(configManager.isIgnoredError('Script error')).toBe(true); + }); + + it('should ignore matching regex', () => { + expect(configManager.isIgnoredError('Script error: something')).toBe(true); + }); + + it('should not ignore non-matching message', () => { + expect(configManager.isIgnoredError('TypeError: something')).toBe(false); + }); + + it('should handle string pattern', () => { + expect(configManager.isIgnoredError('Some ResizeObserver issue')).toBe(true); + }); + }); + + describe('applyToEvent', () => { + beforeEach(() => { + configManager = new ConfigManager({ + dsn: 'https://proj_abc123@log.example.com/1001', + release: '1.0.0', + environment: 'production', + user: { id: '123' }, + tags: { page: 'home' }, + }); + }); + + it('should apply release to event', () => { + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.release).toBe('1.0.0'); + }); + + it('should apply environment to event', () => { + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.environment).toBe('production'); + }); + + it('should apply user to event', () => { + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.user?.id).toBe('123'); + }); + + it('should apply and merge tags', () => { + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + tags: { custom: 'value' }, + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.tags?.page).toBe('home'); + expect(result.tags?.custom).toBe('value'); + }); + + it('should add context_id to event', () => { + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.context_id).toBe('test_context_id'); + }); + + it('should add contexts on first event', () => { + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.contexts).toBeDefined(); + expect(result.contexts?.browser).toBeDefined(); + }); + + it('should add request info to event', () => { + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.request?.url).toBe('https://example.com/page'); + expect(result.request?.referrer).toBe('https://google.com'); + }); + + it('should not overwrite existing request.url', () => { + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + request: { url: 'https://custom.com/api' }, + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.request?.url).toBe('https://custom.com/api'); + }); + }); + + describe('markContextReported', () => { + beforeEach(() => { + configManager = new ConfigManager({ + dsn: 'https://proj_abc123@log.example.com/1001', + }); + }); + + it('should add contexts on first event', () => { + configManager.markContextReported(); + + const event: SentryEvent = { + type: 'error', + level: 'error', + timestamp: Date.now(), + } as ErrorEvent; + const result = configManager.applyToEvent(event); + expect(result.contexts).toBeUndefined(); + expect(result.context_id).toBe('test_context_id'); + }); + }); +}); diff --git a/tests/dsn.test.ts b/tests/dsn.test.ts new file mode 100644 index 0000000..6fc2244 --- /dev/null +++ b/tests/dsn.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest'; +import { parseDSN, getEnvelopeUrl } from '../src/utils/dsn'; + +describe('dsn utils', () => { + describe('parseDSN', () => { + it('should parse valid DSN correctly', () => { + const dsn = parseDSN('https://proj_abc123@log.example.com/1001'); + expect(dsn.protocol).toBe('https'); + expect(dsn.publicKey).toBe('proj_abc123'); + expect(dsn.host).toBe('log.example.com'); + expect(dsn.projectId).toBe('1001'); + }); + + it('should parse DSN with http protocol', () => { + const dsn = parseDSN('http://proj_abc123@log.example.com/1001'); + expect(dsn.protocol).toBe('http'); + }); + + it('should parse DSN with numeric public key', () => { + const dsn = parseDSN('https://123456@log.example.com/1001'); + expect(dsn.publicKey).toBe('123456'); + }); + + it('should throw error for invalid DSN', () => { + expect(() => parseDSN('invalid-dsn')).toThrow(); + }); + + it('should throw error for DSN without project ID', () => { + expect(() => parseDSN('https://proj_abc123@log.example.com')).toThrow(); + }); + }); + + describe('getEnvelopeUrl', () => { + it('should return correct envelope URL', () => { + const dsn = { + protocol: 'https', + publicKey: 'proj_abc123', + host: 'log.example.com', + projectId: '1001', + }; + const url = getEnvelopeUrl(dsn); + expect(url).toContain('https://log.example.com/api/1001/envelope/'); + expect(url).toContain('sentry_key=proj_abc123'); + }); + + it('should use http for non-https protocol', () => { + const dsn = { + protocol: 'http', + publicKey: 'proj_abc123', + host: 'log.example.com', + projectId: '1001', + }; + const url = getEnvelopeUrl(dsn); + expect(url).toContain('http://log.example.com'); + }); + }); +}); diff --git a/tests/env.test.ts b/tests/env.test.ts new file mode 100644 index 0000000..3c13165 --- /dev/null +++ b/tests/env.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect } from 'vitest'; +import { + getEnvInfo, + getContexts, + getContextId, + getPageUrl, + getReferrer, + sanitizeUrl, + shouldIgnoreUrl, +} from '../src/utils/env'; + +describe('env utils', () => { + describe('getEnvInfo', () => { + it('should return env info object', () => { + const env = getEnvInfo(); + expect(env).toBeDefined(); + expect(env).toHaveProperty('browser'); + expect(env).toHaveProperty('os'); + expect(env).toHaveProperty('device'); + }); + + it('should have valid browser info', () => { + const env = getEnvInfo(); + expect(env.browser.name).toBeDefined(); + expect(env.browser.version).toBeDefined(); + expect(env.browser.major).toBeDefined(); + }); + + it('should have valid os info', () => { + const env = getEnvInfo(); + expect(env.os.name).toBeDefined(); + expect(env.os.version).toBeDefined(); + }); + + it('should have valid device info', () => { + const env = getEnvInfo(); + expect(['desktop', 'mobile', 'tablet']).toContain(env.device.type); + }); + }); + + describe('getContexts', () => { + it('should return contexts with encoded values', () => { + const contexts = getContexts(); + expect(contexts).toBeDefined(); + expect(contexts.browser).toBeDefined(); + expect(contexts.os).toBeDefined(); + expect(contexts.device).toBeDefined(); + }); + + it('should use short codes for browser', () => { + const contexts = getContexts(); + expect(contexts.browser.name.length).toBeLessThanOrEqual(10); + }); + + it('should use short codes for os', () => { + const contexts = getContexts(); + expect(contexts.os.name.length).toBeLessThanOrEqual(10); + }); + }); + + describe('getContextId', () => { + it('should return a 16 character string', () => { + const contextId = getContextId(); + expect(contextId).toBeDefined(); + expect(contextId.length).toBe(16); + expect(contextId).toMatch(/^[a-f0-9]+$/i); + }); + + it('should return consistent id for same environment', () => { + const id1 = getContextId(); + const id2 = getContextId(); + expect(id1).toBe(id2); + }); + }); + + describe('getPageUrl', () => { + it('should return current page URL or empty string', () => { + const url = getPageUrl(); + expect(typeof url).toBe('string'); + }); + }); + + describe('getReferrer', () => { + it('should return referrer or empty string', () => { + const referrer = getReferrer(); + expect(typeof referrer).toBe('string'); + }); + }); + + describe('sanitizeUrl', () => { + it('should filter sensitive parameters', () => { + const url = 'https://example.com/api?user=123&password=secret&token=abc'; + const sanitized = sanitizeUrl(url); + expect(sanitized).toContain('password'); + expect(sanitized).toContain('token'); + // URL encoded [Filtered] = %5BFiltered%5D + expect(sanitized).toContain('%5BFiltered%5D'); + }); + + it('should handle URLs without sensitive params', () => { + const url = 'https://example.com/api?page=1&limit=10'; + const sanitized = sanitizeUrl(url); + expect(sanitized).toBe(url); + }); + + it('should handle invalid URLs', () => { + const sanitized = sanitizeUrl('not-a-url'); + expect(sanitized).toBe('not-a-url'); + }); + + it('should filter api_key parameter', () => { + const url = 'https://api.com/endpoint?api_key=secret123'; + const sanitized = sanitizeUrl(url); + expect(sanitized).toContain('api_key'); + expect(sanitized).toContain('%5BFiltered%5D'); + }); + + it('should filter authorization headers in URL', () => { + const url = 'https://example.com/api?code=auth123'; + const sanitized = sanitizeUrl(url); + expect(sanitized).toContain('code'); + expect(sanitized).toContain('%5BFiltered%5D'); + }); + }); + + describe('shouldIgnoreUrl', () => { + it('should return false for empty patterns', () => { + const result = shouldIgnoreUrl('https://example.com/api', []); + expect(result).toBe(false); + }); + + it('should match string pattern', () => { + const result = shouldIgnoreUrl('https://example.com/api/health', ['/health']); + expect(result).toBe(true); + }); + + it('should not match non-matching pattern', () => { + const result = shouldIgnoreUrl('https://example.com/api/users', ['/health']); + expect(result).toBe(false); + }); + + it('should match wildcard pattern', () => { + const result = shouldIgnoreUrl('https://example.com/api/v1/users', ['/api/v1/*']); + expect(result).toBe(true); + }); + + it('should not match wildcard when no wildcard in URL', () => { + const result = shouldIgnoreUrl('https://example.com/api/v2/users', ['/api/v1/*']); + expect(result).toBe(false); + }); + + it('should match regex pattern', () => { + const result = shouldIgnoreUrl('https://example.com/api/users/123', [/api\/users\/\d+/]); + expect(result).toBe(true); + }); + }); +}); diff --git a/tests/hash.test.ts b/tests/hash.test.ts new file mode 100644 index 0000000..d1233b3 --- /dev/null +++ b/tests/hash.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { hashString, md5, computeFingerprint } from '../src/utils/hash'; + +describe('hash utils', () => { + describe('hashString', () => { + it('should return a positive number', () => { + const result = hashString('test'); + expect(result).toBeGreaterThan(0); + }); + + it('should return consistent results for same input', () => { + const result1 = hashString('test'); + const result2 = hashString('test'); + expect(result1).toBe(result2); + }); + + it('should return different results for different inputs', () => { + const result1 = hashString('test1'); + const result2 = hashString('test2'); + expect(result1).not.toBe(result2); + }); + }); + + describe('md5', () => { + it('should return a hex string', () => { + const result = md5('test'); + expect(result).toMatch(/^[a-f0-9]+$/i); + }); + + it('should return consistent results for same input', () => { + const result1 = md5('test'); + const result2 = md5('test'); + expect(result1).toBe(result2); + }); + + it('should return different results for different inputs', () => { + const result1 = md5('test1'); + const result2 = md5('test2'); + expect(result1).not.toBe(result2); + }); + + it('should handle empty string', () => { + const result = md5(''); + expect(result).toBeDefined(); + expect(result.length).toBeGreaterThan(0); + }); + }); + + describe('computeFingerprint', () => { + it('should generate fingerprint for error', () => { + const frames = [ + { filename: 'app.js', lineno: 10 }, + { filename: 'main.js', lineno: 5 }, + ]; + const result = computeFingerprint('TypeError', 'test error', frames); + expect(result).toBeDefined(); + expect(result.length).toBeGreaterThan(0); + }); + + it('should normalize message by removing numbers', () => { + const frames: { filename: string; lineno?: number }[] = []; + const result1 = computeFingerprint('Error', 'User 123 not found', frames); + const result2 = computeFingerprint('Error', 'User 456 not found', frames); + expect(result1).toBe(result2); + }); + + it('should only use first 3 frames', () => { + const frames = [ + { filename: 'a.js', lineno: 1 }, + { filename: 'b.js', lineno: 2 }, + { filename: 'c.js', lineno: 3 }, + { filename: 'd.js', lineno: 4 }, + { filename: 'e.js', lineno: 5 }, + ]; + const result = computeFingerprint('Error', 'message', frames); + expect(result).toBeDefined(); + }); + + it('should handle empty frames', () => { + const result = computeFingerprint('Error', 'message', []); + expect(result).toBeDefined(); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0ca4bc0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2018", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ES2018", "DOM", "DOM.Iterable"], + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationDir": "./dist", + "outDir": "./dist", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fbe7375 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.d.ts'], + }, + }, +});