@mosovn/echo 0.0.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,55 +1,52 @@
1
1
  # @mosovn/echo
2
2
 
3
- Web SDK for the Echo event-tracking system. Built for moso products but
4
- self-contained — works with any Echo-compatible backend.
3
+ SDK for the Echo event-tracking system. **Works in both browser and React Native** — bundler auto-picks the right implementation via conditional exports.
5
4
 
6
5
  ```bash
7
- pnpm add @mosovn/echo
6
+ # Web (Next.js / Vite / Webpack / etc.)
7
+ npm install @mosovn/echo
8
+
9
+ # React Native — also install the platform polyfills
10
+ npm install @mosovn/echo \
11
+ @react-native-async-storage/async-storage \
12
+ react-native-get-random-values
13
+
14
+ # iOS only
15
+ cd ios && pod install && cd ..
8
16
  ```
9
17
 
18
+ ## Web usage
19
+
10
20
  ```ts
11
21
  import { createEcho } from '@mosovn/echo';
12
22
 
13
23
  const echo = createEcho({
14
24
  writeKey: 'pk_xxxxxxxxxxxx',
15
- endpoint: 'https://events.moso.vn', // optional, this is the default
16
25
  });
17
26
 
18
- // Custom events
19
27
  echo.track('Sign Up', { plan: 'pro' });
20
-
21
- // Identify a logged-in user
22
28
  echo.identify('user_123', { email: 'foo@moso.vn' });
23
-
24
- // Update traits later
25
- echo.setUserProperties({ company: 'moso' });
26
-
27
- // Logout
28
- echo.reset();
29
29
  ```
30
30
 
31
- ## Autocapture
31
+ ### Web autocapture (default ON)
32
32
 
33
- Out of the box the SDK captures:
34
-
35
- - `$pageview` — page loads and SPA navigation
36
- - `$click` — every click (use `data-no-track` to exclude an element)
37
- - `$form_submit` — form submissions (never field values)
38
- - `$scroll` — scroll depth, throttled to 250 ms + ≥10% delta
33
+ - `$pageview` page load + SPA navigation (pushState/popstate/hashchange)
34
+ - `$click` — every click; use `data-no-track` to exclude
35
+ - `$form_submit` — form submit (never captures field values)
36
+ - `$scroll` — throttled to 250 ms + ≥10% delta
39
37
  - `$visibility` — tab focus/blur
40
- - `$hover` — mouseenter on interactive elements (a, button, [data-track]),
41
- throttled to 500 ms
42
- - `$error` — `window.onerror` + `unhandledrejection`, deduped 5 s
38
+ - `$hover` — mouseenter on a/button/[id]/[role=button], 500 ms throttle
39
+ - `$error` `window.onerror` + `unhandledrejection`, 5 s dedup
43
40
  - `$web_vital` — LCP, FID, CLS
44
41
  - `$session_start` / `$session_end` — 30 min inactivity timeout
45
42
 
46
- Configure or disable selectively:
43
+ Configure or disable:
47
44
 
48
45
  ```ts
49
46
  createEcho({
50
47
  writeKey: 'pk_xxx',
51
48
  autoCapture: {
52
- clicks: 'instrumented', // only a/button/[data-track]
49
+ clicks: 'instrumented', // only a/button/[id]/[role=button]
53
50
  scroll: false,
54
51
  },
55
52
  maskTextSelectors: ['input', '[data-private]'],
@@ -57,22 +54,106 @@ createEcho({
57
54
  });
58
55
  ```
59
56
 
57
+ ## React Native usage
58
+
59
+ In your app entry (`index.js` or `App.tsx`), import the polyfill **FIRST**:
60
+
61
+ ```js
62
+ // index.js — TOP OF FILE
63
+ import 'react-native-get-random-values';
64
+ ```
65
+
66
+ Then anywhere:
67
+
68
+ ```ts
69
+ import { createEcho } from '@mosovn/echo';
70
+
71
+ const echo = createEcho({
72
+ writeKey: 'pk_xxxxxxxxxxxx',
73
+ app: {
74
+ name: 'MOSO Mobile',
75
+ version: '2.4.1',
76
+ build: '124',
77
+ },
78
+ });
79
+
80
+ echo.track('Sign Up', { plan: 'pro' });
81
+ echo.identify('user_123', { email: 'foo@moso.vn' });
82
+ ```
83
+
84
+ ### RN autocapture
85
+
86
+ Limited (no DOM in RN):
87
+
88
+ - `$session_start` / `$session_end` — `AppState` foreground/background + 30 min inactivity
89
+ - `$error` — `ErrorUtils.setGlobalHandler` + `unhandledrejection`
90
+
91
+ You must call manually:
92
+
93
+ - Screen views — hook into React Navigation:
94
+ ```tsx
95
+ <NavigationContainer
96
+ ref={navRef}
97
+ onStateChange={() => {
98
+ const r = navRef.getCurrentRoute();
99
+ if (r) echo.screen?.(r.name);
100
+ }}
101
+ >
102
+ ```
103
+ - Button taps, form submits, network errors — call `echo.track()` in your handlers
104
+
105
+ ## API
106
+
107
+ | Method | Web | RN | Notes |
108
+ |---|---|---|---|
109
+ | `createEcho(config)` | ✅ | ✅ | Returns `EchoClient` |
110
+ | `echo.track(name, props?)` | ✅ | ✅ | Custom event |
111
+ | `echo.identify(userId, traits?)` | ✅ | ✅ | Set user identity |
112
+ | `echo.setUserProperties(traits)` | ✅ | ✅ | Update traits |
113
+ | `echo.page(props?)` | ✅ | n/a | Web only — URL-based |
114
+ | `echo.screen(name, props?)` | n/a | ✅ | RN only — call from navigation |
115
+ | `echo.reset()` | sync | async | Logout; rotates anonymousId |
116
+ | `echo.flush()` | ✅ | ✅ | Force send queued events |
117
+ | `echo.getAnonymousId()` | ✅ | ✅ | |
118
+ | `echo.getUserId()` | ✅ | ✅ | |
119
+ | `echo.getSessionId()` | ✅ | ✅ | |
120
+
121
+ ## How bundler picks the right entry
122
+
123
+ `@mosovn/echo` ships two builds in `dist/`:
124
+
125
+ - `dist/web.js` — uses `localStorage`, `document`, `window`, `MutationObserver`, `PerformanceObserver`
126
+ - `dist/native.js` — uses `AsyncStorage`, `AppState`, `ErrorUtils`, `react-native-get-random-values`
127
+
128
+ `package.json` `exports` field routes:
129
+
130
+ ```json
131
+ {
132
+ "exports": {
133
+ ".": {
134
+ "react-native": "./dist/native.js", // ← Metro picks this
135
+ "browser": "./dist/web.js", // ← Webpack/Vite picks this
136
+ "import": "./dist/web.js", // ← ESM default
137
+ "require": "./dist/web.cjs" // ← CJS default
138
+ }
139
+ },
140
+ "react-native": "./dist/native.js" // ← older Metro fallback
141
+ }
142
+ ```
143
+
144
+ Same `import { createEcho } from '@mosovn/echo'` in your code works in both environments.
145
+
60
146
  ## PII
61
147
 
62
- Form field values are NEVER captured. Text inside `<input>`, `<textarea>`,
63
- `<select>`, and `[data-private]` is masked from autocaptured events by default.
148
+ Form field values are NEVER captured. Text inside `<input>`, `<textarea>`, `<select>`, and `[data-private]` is masked from autocaptured events by default.
64
149
 
65
- Drop a `data-no-track` attribute on any element to exclude it (and its
66
- descendants) from all autocapture.
150
+ `data-no-track` attribute on any element excludes it (and its descendants) from all autocapture.
67
151
 
68
152
  ## Identity
69
153
 
70
- - **anonymous_id** — stable across sessions, stored in localStorage + a cookie
71
- on the apex domain so subdomains share it.
154
+ - **anonymous_id** — UUIDv7, persistent across sessions. Web: localStorage + cookie on apex domain. RN: AsyncStorage.
72
155
  - **user_id** — set by `identify()`, cleared by `reset()`.
73
- - **session_id** — rotates after 30 min of inactivity.
74
-
75
- `echo.reset()` rotates the anonymous_id too, so use it on logout.
156
+ - **session_id** — rotates after 30 min inactivity. Web: per-tab. RN: per-app.
76
157
 
77
158
  ## License
78
159
 
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared types for both `@mosovn/echo` entries (web + native).
3
+ * Platform-specific options are tagged in JSDoc; setting a web-only option in
4
+ * RN (or vice versa) is silently ignored, not an error.
5
+ */
6
+ interface EchoConfig {
7
+ /** Project's public write key (e.g. `pk_xxx`). */
8
+ writeKey: string;
9
+ /** API endpoint base, defaults to `https://events.moso.vn`. */
10
+ endpoint?: string;
11
+ /** Number of events to buffer before forcing a flush. Default 50. */
12
+ flushAt?: number;
13
+ /** Max time (ms) to wait before flushing the buffer. Default 10000. */
14
+ flushInterval?: number;
15
+ /** Optional middleware called on every event before queueing. Return `null` to drop. */
16
+ beforeSend?: (event: EchoEvent) => EchoEvent | null;
17
+ /** Inactivity timeout (ms) to end a session. Default 30 min. */
18
+ sessionTimeoutMs?: number;
19
+ /** If true, log internal diagnostic info to console. */
20
+ debug?: boolean;
21
+ /** [Web only] Where to persist the queue. Default 'localstorage'. */
22
+ storage?: 'indexeddb' | 'localstorage' | 'memory';
23
+ /** [Web only] Autocapture configuration (DOM-based). See AutoCaptureConfig. */
24
+ autoCapture?: AutoCaptureConfig | boolean;
25
+ /** [Web only] CSS selectors whose text content is masked from captured events. */
26
+ maskTextSelectors?: string[];
27
+ /** [Web only] CSS selectors to skip during autocapture. */
28
+ blockSelectors?: string[];
29
+ /** [Native only] App info attached to event context. */
30
+ app?: {
31
+ name?: string;
32
+ version?: string;
33
+ build?: string;
34
+ };
35
+ /** [Native only] Track AppState transitions as $session_start/end. Default true. */
36
+ trackSessions?: boolean;
37
+ /** [Native only] Track JS errors via ErrorUtils + unhandledrejection. Default true. */
38
+ trackErrors?: boolean;
39
+ }
40
+ interface AutoCaptureConfig {
41
+ pageviews?: boolean;
42
+ /** 'instrumented' = a/button/[role=button]/[id]. 'all' = every click. */
43
+ clicks?: boolean | 'instrumented' | 'all';
44
+ forms?: boolean;
45
+ scroll?: boolean;
46
+ visibility?: boolean;
47
+ mouseover?: boolean;
48
+ errors?: boolean;
49
+ vitals?: boolean;
50
+ sessions?: boolean;
51
+ }
52
+ interface EchoEvent {
53
+ /** UUID v7. Client-generated. Doubles as dedup key on the server. */
54
+ _id: string;
55
+ eventName: string;
56
+ eventType: 'auto' | 'custom';
57
+ anonymousId: string;
58
+ userId?: string | null;
59
+ timestamp: string;
60
+ sessionId?: string;
61
+ properties?: Record<string, unknown>;
62
+ context?: Record<string, unknown>;
63
+ }
64
+ interface EchoClient {
65
+ /** Track a custom event. */
66
+ track(eventName: string, properties?: Record<string, unknown>): void;
67
+ /** Identify the current user. Call after login. */
68
+ identify(userId: string, traits?: Record<string, unknown>): void;
69
+ /** Update user traits. */
70
+ setUserProperties(traits: Record<string, unknown>): void;
71
+ /** [Web only] Record a page view (URL-based). No-op on native. */
72
+ page?(properties?: Record<string, unknown>): void;
73
+ /** [Native only] Record a screen view. No-op on web. */
74
+ screen?(screenName: string, properties?: Record<string, unknown>): void;
75
+ /** Clear identity (call on logout). Rotates anonymousId. */
76
+ reset(): void | Promise<void>;
77
+ /** Force flush queued events now. */
78
+ flush(): Promise<void>;
79
+ getAnonymousId(): string;
80
+ getUserId(): string | null;
81
+ getSessionId(): string;
82
+ }
83
+
84
+ /**
85
+ * @mosovn/echo — React Native entry.
86
+ *
87
+ * Metro picks this file via the `react-native` field in `package.json`.
88
+ * Web bundlers (Webpack / Vite / Next.js) load `./web.ts` instead.
89
+ *
90
+ * Usage:
91
+ *
92
+ * // In your app entry — FIRST line, before other imports:
93
+ * import 'react-native-get-random-values';
94
+ *
95
+ * import { createEcho } from '@mosovn/echo';
96
+ *
97
+ * const echo = createEcho({
98
+ * writeKey: 'pk_xxx',
99
+ * app: { name: 'MOSO Mobile', version: '2.4.1' },
100
+ * });
101
+ *
102
+ * echo.track('Sign Up', { plan: 'pro' });
103
+ * echo.identify('user_123', { email: 'foo@moso.vn' });
104
+ */
105
+
106
+ declare function createEcho(config: EchoConfig): EchoClient;
107
+
108
+ export { type AutoCaptureConfig, type EchoClient, type EchoConfig, type EchoEvent, createEcho };
package/dist/native.js ADDED
@@ -0,0 +1,2 @@
1
+ import {Platform,AppState}from'react-native';import E from'@react-native-async-storage/async-storage';var f=new Map;async function w(s){try{let t=await E.multiGet(s);for(let[e,i]of t)i!==null&&f.set(e,i);}catch{}}function u(s){return f.get(s)??null}function r(s,t){f.set(s,t),E.setItem(s,t).catch(()=>{});}function l(s){f.delete(s),E.removeItem(s).catch(()=>{});}function a(){let s=BigInt(Date.now()),t=new Uint8Array(10);crypto.getRandomValues(t);let e=new Uint8Array(16);e[0]=Number(s>>40n&0xffn),e[1]=Number(s>>32n&0xffn),e[2]=Number(s>>24n&0xffn),e[3]=Number(s>>16n&0xffn),e[4]=Number(s>>8n&0xffn),e[5]=Number(s&0xffn),e[6]=t[0]&15|112,e[7]=t[1],e[8]=t[2]&63|128,e[9]=t[3],e[10]=t[4],e[11]=t[5],e[12]=t[6],e[13]=t[7],e[14]=t[8],e[15]=t[9];let i=Array.from(e,n=>n.toString(16).padStart(2,"0")).join("");return `${i.slice(0,8)}-${i.slice(8,12)}-${i.slice(12,16)}-${i.slice(16,20)}-${i.slice(20)}`}var g="echo_anonymous_id",p="echo_user_id",h="echo_session_id",d="echo_last_activity",b=[g,p,h,d],m=class{constructor(t){this.sessionTimeoutMs=t;let e=u(g);e||(e=a(),r(g,e)),this.anonymousId=e,this.userId=u(p);let i=Date.now(),n=u(h),o=parseInt(u(d)||"0",10);n&&i-o<t?this.sessionId=n:(this.sessionId=a(),r(h,this.sessionId)),this.lastActivity=i,r(d,String(i));}touch(){let t=Date.now(),e=t-this.lastActivity>=this.sessionTimeoutMs,i;return e&&(i=this.sessionId,this.sessionId=a(),r(h,this.sessionId)),this.lastActivity=t,r(d,String(t)),{sessionStarted:e,oldSessionId:i}}setUserId(t){this.userId=t,r(p,t);}reset(){this.userId=null,l(p),this.anonymousId=a(),r(g,this.anonymousId),this.sessionId=a(),r(h,this.sessionId),this.lastActivity=Date.now(),r(d,String(this.lastActivity));}getAnonymousId(){return this.anonymousId}getUserId(){return this.userId}getSessionId(){return this.sessionId}};var I="echo_queue_v1",_=1e3,y=class{constructor(){this.buffer=[];}restore(){try{let t=u(I);t&&(this.buffer=JSON.parse(t));}catch{this.buffer=[];}}persist(){try{r(I,JSON.stringify(this.buffer));}catch{}}enqueue(t){this.buffer.length>=_&&this.buffer.shift(),this.buffer.push(t),this.persist();}size(){return this.buffer.length}drain(){let t=this.buffer;return this.buffer=[],l(I),t}requeue(t){this.buffer=t.concat(this.buffer).slice(0,_),this.persist();}};async function A(s,t){if(s.length===0)return true;let e=t.endpoint.replace(/\/+$/,"")+"/v1/events";try{return (await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":t.writeKey},body:JSON.stringify({events:s})})).ok}catch{return false}}async function k(s,t){let e=t.endpoint.replace(/\/+$/,"")+"/v1/identify";try{return (await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":t.writeKey},body:JSON.stringify(s)})).ok}catch{return false}}var U="https://events.moso.vn",q=50,x=1e4,K=1800*1e3,C=["echo_queue_v1"],v=class{constructor(t){this.flushTimer=null;this.flushing=false;this.ready=false;if(!t.writeKey)throw new Error("@mosovn/echo-rn: writeKey is required");this.config={writeKey:t.writeKey,endpoint:t.endpoint??U,flushAt:t.flushAt??q,flushInterval:t.flushInterval??x,sessionTimeoutMs:t.sessionTimeoutMs??K,debug:t.debug??false,trackSessions:t.trackSessions??true,trackErrors:t.trackErrors??true,...t},this.init();}debugLog(...t){this.config.debug&&console.log("[echo-rn]",...t);}async init(){await w([...b,...C]),this.identity=new m(this.config.sessionTimeoutMs),this.queue=new y,this.queue.restore(),this.ready=true,this.debugLog("init complete",{anon:this.identity.getAnonymousId(),session:this.identity.getSessionId()}),this.startFlushTimer(),this.config.trackSessions&&this.installAppStateTracking(),this.config.trackErrors&&this.installErrorTracking();}track(t,e){this.enqueueEvent(t,"custom",e);}identify(t,e){if(!this.ready){this.waitReady().then(()=>this.identify(t,e));return}this.identity.setUserId(t),this.enqueueEvent("$identify","auto",e),k({userId:t,anonymousId:this.identity.getAnonymousId(),traits:e||{}},{writeKey:this.config.writeKey,endpoint:this.config.endpoint});}setUserProperties(t){if(!this.ready){this.waitReady().then(()=>this.setUserProperties(t));return}let e=this.identity.getUserId();e?this.identify(e,t):this.enqueueEvent("$set_user_properties","auto",t);}screen(t,e){this.enqueueEvent("$screen","auto",{name:t,...e});}async reset(){await this.waitReady(),this.identity.reset(),this.queue.drain(),this.debugLog("reset done");}async flush(){if(await this.waitReady(),this.flushing||this.queue.size()===0)return;this.flushing=true;let t=this.queue.drain();await A(t,{writeKey:this.config.writeKey,endpoint:this.config.endpoint})?this.debugLog("flush ok",t.length):(this.queue.requeue(t),this.debugLog("flush failed, requeued",t.length)),this.flushing=false;}getAnonymousId(){return this.ready?this.identity.getAnonymousId():""}getUserId(){return this.ready?this.identity.getUserId():null}getSessionId(){return this.ready?this.identity.getSessionId():""}waitReady(){return this.ready?Promise.resolve():new Promise(t=>{let e=()=>this.ready?t():setTimeout(e,50);e();})}enqueueEvent(t,e,i){if(!this.ready){this.waitReady().then(()=>this.enqueueEvent(t,e,i));return}let{sessionStarted:n,oldSessionId:o}=this.identity.touch();n&&o&&(this.pushRaw("$session_end","auto",void 0,o),this.pushRaw("$session_start","auto")),this.pushRaw(t,e,i);}pushRaw(t,e,i,n){let o={_id:a(),eventName:t,eventType:e,anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:n??this.identity.getSessionId(),properties:i,context:this.collectContext()},S=this.config.beforeSend,c=S?S(o):o;c&&(this.queue.enqueue(c),this.debugLog("enqueue",c.eventName,c._id),this.queue.size()>=this.config.flushAt&&this.flush());}collectContext(){let t={lib:{name:"@mosovn/echo",version:"0.2.0"},os:{name:Platform.OS,version:String(Platform.Version)}};return this.config.app&&(t.app=this.config.app),t}startFlushTimer(){this.flushTimer=setInterval(()=>{this.flush();},this.config.flushInterval);}installAppStateTracking(){let t=AppState.currentState;this.appStateSub=AppState.addEventListener("change",e=>{e!==t&&(this.debugLog("appstate",t,"\u2192",e),(e==="background"||e==="inactive")&&this.flush(),t=e);});}installErrorTracking(){let t=globalThis.ErrorUtils;if(t){let i=t.getGlobalHandler();t.setGlobalHandler((n,o)=>{this.track("$error",{message:n?.message,stack:n?.stack,fatal:!!o}),i?.(n,o);});}let e=i=>{let n=i?.reason;this.track("$error",{kind:"unhandledrejection",message:n instanceof Error?n.message:String(n),stack:n instanceof Error?n.stack:void 0});};globalThis.addEventListener?.("unhandledrejection",e);}};function X(s){return new v(s)}
2
+ export{X as createEcho};
@@ -1,2 +1,2 @@
1
- 'use strict';function a(){let i=BigInt(Date.now()),e=crypto.getRandomValues(new Uint8Array(10)),t=new Uint8Array(16);t[0]=Number(i>>40n&0xffn),t[1]=Number(i>>32n&0xffn),t[2]=Number(i>>24n&0xffn),t[3]=Number(i>>16n&0xffn),t[4]=Number(i>>8n&0xffn),t[5]=Number(i&0xffn),t[6]=e[0]&15|112,t[7]=e[1],t[8]=e[2]&63|128,t[9]=e[3],t[10]=e[4],t[11]=e[5],t[12]=e[6],t[13]=e[7],t[14]=e[8],t[15]=e[9];let n=Array.from(t,r=>r.toString(16).padStart(2,"0")).join("");return `${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`}var f="echo_anonymous_id",w="echo_user_id",d="echo_session_id",h="echo_last_activity";function O(){try{return typeof localStorage>"u"?null:(localStorage.setItem("__echo_probe__","1"),localStorage.removeItem("__echo_probe__"),localStorage)}catch{return null}}function D(){if(typeof location>"u")return null;let i=location.hostname;if(i==="localhost"||/^\d+\.\d+\.\d+\.\d+$/.test(i))return null;let e=i.split(".");return e.length<2?null:"."+e.slice(-2).join(".")}function N(i){if(typeof document>"u")return null;let e=document.cookie.match(new RegExp("(?:^|; )"+i+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}function P(i,e,t=365){if(typeof document>"u")return;let n=D(),r=new Date(Date.now()+t*864e5).toUTCString(),o=[`${i}=${encodeURIComponent(e)}`,`expires=${r}`,"path=/","SameSite=Lax"];n&&o.push(`domain=${n}`),typeof location<"u"&&location.protocol==="https:"&&o.push("Secure"),document.cookie=o.join("; ");}var m=class{constructor(e){this.storage=O();this.sessionTimeoutMs=e;let t=N(f)||this.storage?.getItem(f)||null;t||(t=a()),this.anonymousId=t,this.persistAnon(),this.userId=this.storage?.getItem(w)||null;let n=Date.now(),r=this.storage?.getItem(d),o=parseInt(this.storage?.getItem(h)||"0",10);r&&n-o<e?this.sessionId=r:(this.sessionId=a(),this.storage?.setItem(d,this.sessionId)),this.lastActivity=n,this.storage?.setItem(h,String(n));}persistAnon(){this.storage?.setItem(f,this.anonymousId),P(f,this.anonymousId);}touch(){let e=Date.now(),t=e-this.lastActivity>=this.sessionTimeoutMs,n;return t&&(n=this.sessionId,this.sessionId=a(),this.storage?.setItem(d,this.sessionId)),this.lastActivity=e,this.storage?.setItem(h,String(e)),{sessionStarted:t,oldSessionId:n}}setUserId(e){this.userId=e,this.storage?.setItem(w,e);}reset(){this.userId=null,this.storage?.removeItem(w),this.anonymousId=a(),this.persistAnon(),this.sessionId=a(),this.storage?.setItem(d,this.sessionId),this.lastActivity=Date.now(),this.storage?.setItem(h,String(this.lastActivity));}getAnonymousId(){return this.anonymousId}getUserId(){return this.userId}getSessionId(){return this.sessionId}};var p="echo_queue_v1";var g=class{constructor(e){this.buffer=[];this.storage=e==="localstorage"&&typeof localStorage<"u"?localStorage:null,this.restore();}restore(){if(this.storage)try{let e=this.storage.getItem(p);e&&(this.buffer=JSON.parse(e));}catch{this.buffer=[];}}persist(){if(this.storage)try{this.storage.setItem(p,JSON.stringify(this.buffer));}catch{for(;this.buffer.length>50;){this.buffer.shift();try{this.storage.setItem(p,JSON.stringify(this.buffer));break}catch{}}}}enqueue(e){this.buffer.length>=1e3&&this.buffer.shift(),this.buffer.push(e),this.persist();}size(){return this.buffer.length}drain(){let e=this.buffer;return this.buffer=[],this.storage&&this.storage.removeItem(p),e}requeue(e){this.buffer=e.concat(this.buffer).slice(0,1e3),this.persist();}};async function I(i,e){if(i.length===0)return true;let t=e.endpoint.replace(/\/+$/,"")+"/v1/events",n=JSON.stringify({events:i});if(e.useBeacon&&typeof navigator<"u"&&navigator.sendBeacon){let r=`${t}?writeKey=${encodeURIComponent(e.writeKey)}`,o=new Blob([n],{type:"application/json"});return navigator.sendBeacon(r,o)}try{return (await fetch(t,{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":e.writeKey},body:n,keepalive:!0,credentials:"omit"})).ok}catch{return false}}var K="https://events.moso.vn",F=50,H=1e4,B=1800*1e3,y=class{constructor(e){this.flushTimer=null;this.flushing=false;this.currentUrl="";this.previousUrl="";if(!e.writeKey)throw new Error("@mosovn/echo: writeKey is required");this.config={writeKey:e.writeKey,endpoint:e.endpoint??K,flushAt:e.flushAt??F,flushInterval:e.flushInterval??H,sessionTimeoutMs:e.sessionTimeoutMs??B,debug:e.debug??false,...e},this.identity=new m(this.config.sessionTimeoutMs);let t=e.storage==="memory"?"memory":"localstorage";this.queue=new g(t),typeof location<"u"&&(this.currentUrl=location.href),typeof document<"u"&&(this.previousUrl=document.referrer||""),this.startFlushTimer(),this.installPageHideFlush();}markPageview(e){e!==this.currentUrl&&(this.previousUrl=this.currentUrl,this.currentUrl=e);}getPreviousUrl(){return this.previousUrl}debugLog(...e){this.config.debug&&console.log("[echo]",...e);}buildEvent(e,t,n){let{sessionStarted:r,oldSessionId:o}=this.identity.touch();return r&&o&&(this.enqueueRaw({_id:a(),eventName:"$session_end",eventType:"auto",anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:o}),this.enqueueRaw({_id:a(),eventName:"$session_start",eventType:"auto",anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:this.identity.getSessionId()})),{_id:a(),eventName:e,eventType:t,anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:this.identity.getSessionId(),properties:n,context:this.collectContext()}}collectContext(){let e={lib:{name:"@mosovn/echo",version:"0.0.0"}};return typeof location<"u"&&(e.page={url:location.href,path:location.pathname,referrer:this.previousUrl,title:typeof document<"u"?document.title:""}),typeof navigator<"u"&&(e.locale=navigator.language),typeof screen<"u"&&(e.screen={width:screen.width,height:screen.height}),e}enqueueRaw(e){let t=this.config.beforeSend,n=t?t(e):e;n&&(this.queue.enqueue(n),this.debugLog("enqueue",n.eventName,n._id),this.queue.size()>=this.config.flushAt&&this.flush());}enqueueEvent(e,t,n){let r=this.buildEvent(e,t,n);this.enqueueRaw(r);}track(e,t){this.enqueueEvent(e,"custom",t);}identify(e,t){this.identity.setUserId(e),this.enqueueEvent("$identify","auto",t),fetch(this.config.endpoint.replace(/\/+$/,"")+"/v1/identify",{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":this.config.writeKey},body:JSON.stringify({userId:e,anonymousId:this.identity.getAnonymousId(),traits:t||{}}),keepalive:true,credentials:"omit"}).catch(()=>{});}setUserProperties(e){let t=this.identity.getUserId();t?this.identify(t,e):this.enqueueEvent("$set_user_properties","auto",e);}page(e){this.enqueueEvent("$pageview","auto",e);}reset(){this.identity.reset(),this.queue.drain();}async flush(){if(this.flushing||this.queue.size()===0)return;this.flushing=true;let e=this.queue.drain();await I(e,{writeKey:this.config.writeKey,endpoint:this.config.endpoint})?this.debugLog("flush ok",e.length):(this.queue.requeue(e),this.debugLog("flush failed, requeued",e.length)),this.flushing=false;}getAnonymousId(){return this.identity.getAnonymousId()}getUserId(){return this.identity.getUserId()}getSessionId(){return this.identity.getSessionId()}startFlushTimer(){typeof setInterval>"u"||(this.flushTimer=setInterval(()=>{this.flush();},this.config.flushInterval));}installPageHideFlush(){if(typeof document>"u")return;let e=()=>{let t=this.queue.drain();t.length!==0&&I(t,{writeKey:this.config.writeKey,endpoint:this.config.endpoint,useBeacon:true});};document.addEventListener("pagehide",e),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&e();});}};var j=["input","textarea","select","[data-private]"],V=["[data-no-track]","script","style"];function T(i,e){for(let t of e)try{if(i.matches(t))return !0}catch{}return false}function l(i,e){let t=e.blockSelectors&&e.blockSelectors.length?e.blockSelectors:V;return T(i,t)}function Y(i,e){let t=e.maskTextSelectors&&e.maskTextSelectors.length?e.maskTextSelectors:j;return T(i,t)}function J(i,e){return Y(i,e)?"":(i.innerText||i.textContent||"").replace(/\s+/g," ").trim().slice(0,200)}function S(i){let e=[],t=i,n=0;for(;t&&t.nodeType===1&&n<6;){let r=t.tagName.toLowerCase();if(t.id){e.unshift(`${r}#${t.id}`);break}let o=(t.getAttribute("class")||"").split(/\s+/).filter(Boolean).slice(0,2).map(u=>"."+u).join(""),s="";if(t.parentElement){let u=Array.from(t.parentElement.children).filter(c=>c.tagName===t.tagName);u.length>1&&(s=`:nth-of-type(${u.indexOf(t)+1})`);}e.unshift(`${r}${o}${s}`),t=t.parentElement,n++;}return e.join(" > ")}function E(i,e){let t=i.tagName.toLowerCase(),n={selector:S(i),tag:t,text:J(i,e)},r=i.id;r&&(n.id=r);let o=i.getAttribute&&i.getAttribute("name");if(o&&(n.name=o),t==="a"){let u=i.href;u&&(n.href=u);}let s=i.getAttribute&&i.getAttribute("aria-label");return s&&(n.ariaLabel=s),n}function v(i){let e=i.tagName?.toLowerCase();if(!e)return false;if(e==="a"||e==="button")return true;let t=i.getAttribute&&i.getAttribute("role");return t==="button"||t==="link"||t==="tab"||t==="menuitem"}function k(i,e,t){typeof document>"u"||document.addEventListener("click",n=>{let r=n.target;if(r){if(e==="instrumented"){let o=r;for(;o&&o!==document.body&&!v(o);)o=o.parentElement;if(!o||o===document.body)return;r=o;}l(r,t)||i.enqueueEvent("$click","auto",E(r,t));}},true);}function A(i){if(typeof window>"u")return;let e=[],t=n=>{let r=Date.now();for(;e.length&&r-e[0].at>5e3;)e.shift();return e.some(o=>o.key===n)?false:(e.push({key:n,at:r}),true)};window.addEventListener("error",n=>{let r=n.message||String(n.error||"unknown"),o=`${r}|${n.filename}|${n.lineno}`;t(o)&&i.enqueueEvent("$error","auto",{message:r,source:n.filename,line:n.lineno,column:n.colno,stack:n.error&&n.error.stack||void 0});}),window.addEventListener("unhandledrejection",n=>{let r=n.reason,o=r instanceof Error?r.message:String(r),s=r instanceof Error?r.stack:void 0,u=`unhandled:${o}`;t(u)&&i.enqueueEvent("$error","auto",{kind:"unhandledrejection",message:o,stack:s});});}function _(i,e){typeof document>"u"||document.addEventListener("submit",t=>{let n=t.target;if(!n||n.tagName!=="FORM"||l(n,e))return;let r=n.querySelectorAll("input, select, textarea").length;i.enqueueEvent("$form_submit","auto",{selector:S(n),id:n.id||void 0,name:n.getAttribute("name")||void 0,action:n.getAttribute("action")||void 0,method:(n.getAttribute("method")||"get").toUpperCase(),fieldCount:r});},true);}var X=500;function x(i,e){if(typeof document>"u")return;let t=null,n=0;document.addEventListener("mouseover",r=>{let o=r.target;if(!o)return;let s=o;for(;s&&s!==document.body&&!v(s);)s=s.parentElement;if(!s||s===document.body||s===t||l(s,e))return;let u=Date.now();if(u-n<X){t=s;return}t=s,n=u,i.enqueueEvent("$hover","auto",E(s,e));},true);}function R(i){if(typeof window>"u"||typeof history>"u")return;let e=location.href,t=0,n=()=>{let o=Date.now();o-t<100||location.href===e&&t!==0||(e=location.href,t=o,i.markPageview(location.href),i.enqueueEvent("$pageview","auto",{url:location.href,path:location.pathname,title:document?.title,referrer:i.getPreviousUrl()}));};n();let r=o=>{let s=history[o];history[o]=function(...u){let c=s.apply(history,u);return n(),c};};r("pushState"),r("replaceState"),window.addEventListener("popstate",n),window.addEventListener("hashchange",n);}function U(i){if(typeof window>"u"||typeof document>"u")return;let e=0,t=0,n=location.href,r=()=>{location.href!==n&&(n=location.href,e=0,t=0);let o=Date.now();if(o-t<250)return;let s=document.documentElement,u=window.scrollY||s.scrollTop||0,c=window.innerHeight||s.clientHeight,C=s.scrollHeight-c;if(C<=0)return;let b=Math.max(0,Math.min(100,Math.round(u/C*100)));b-e<10||(e=b,t=o,i.enqueueEvent("$scroll","auto",{percent:b,pixels:u}));};window.addEventListener("scroll",r,{passive:true});}function L(i){if(typeof document>"u")return;let e=document.visibilityState;document.addEventListener("visibilitychange",()=>{let t=document.visibilityState;t!==e&&(e=t,i.enqueueEvent("$visibility","auto",{state:t}));});}function M(i){if(typeof PerformanceObserver>"u")return;let e=(t,n,r)=>{i.enqueueEvent("$web_vital","auto",{name:t,value:n,...r});};try{new PerformanceObserver(n=>{let r=n.getEntries(),o=r[r.length-1];o&&e("LCP",Math.round(o.startTime));}).observe({type:"largest-contentful-paint",buffered:!0});}catch{}try{let t=0;new PerformanceObserver(r=>{for(let o of r.getEntries())o.hadRecentInput||(t+=o.value);}).observe({type:"layout-shift",buffered:!0}),addEventListener("pagehide",()=>e("CLS",Math.round(t*1e3)/1e3));}catch{}try{new PerformanceObserver(n=>{let r=n.getEntries()[0];r&&e("FID",Math.round(r.processingStart-r.startTime));}).observe({type:"first-input",buffered:!0});}catch{}}var q={pageviews:true,clicks:"all",forms:true,scroll:true,visibility:true,mouseover:true,errors:true,vitals:true,sessions:true};function Q(i){return i===false?null:i===void 0||i===true?q:{...q,...i}}function $(i,e){let t=Q(e.autoCapture);if(!t)return;let n={maskTextSelectors:e.maskTextSelectors,blockSelectors:e.blockSelectors};if(t.pageviews&&R(i),t.clicks){let r=t.clicks==="instrumented"?"instrumented":"all";k(i,r,n);}t.forms&&_(i,n),t.scroll&&U(i),t.visibility&&L(i),t.mouseover&&x(i,n),t.errors&&A(i),t.vitals&&M(i);}function xe(i){let e=new y(i);return $(e,i),e}
1
+ 'use strict';function a(){let i=BigInt(Date.now()),e=crypto.getRandomValues(new Uint8Array(10)),t=new Uint8Array(16);t[0]=Number(i>>40n&0xffn),t[1]=Number(i>>32n&0xffn),t[2]=Number(i>>24n&0xffn),t[3]=Number(i>>16n&0xffn),t[4]=Number(i>>8n&0xffn),t[5]=Number(i&0xffn),t[6]=e[0]&15|112,t[7]=e[1],t[8]=e[2]&63|128,t[9]=e[3],t[10]=e[4],t[11]=e[5],t[12]=e[6],t[13]=e[7],t[14]=e[8],t[15]=e[9];let n=Array.from(t,r=>r.toString(16).padStart(2,"0")).join("");return `${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`}var f="echo_anonymous_id",w="echo_user_id",d="echo_session_id",h="echo_last_activity";function O(){try{return typeof localStorage>"u"?null:(localStorage.setItem("__echo_probe__","1"),localStorage.removeItem("__echo_probe__"),localStorage)}catch{return null}}function D(){if(typeof location>"u")return null;let i=location.hostname;if(i==="localhost"||/^\d+\.\d+\.\d+\.\d+$/.test(i))return null;let e=i.split(".");return e.length<2?null:"."+e.slice(-2).join(".")}function N(i){if(typeof document>"u")return null;let e=document.cookie.match(new RegExp("(?:^|; )"+i+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}function P(i,e,t=365){if(typeof document>"u")return;let n=D(),r=new Date(Date.now()+t*864e5).toUTCString(),o=[`${i}=${encodeURIComponent(e)}`,`expires=${r}`,"path=/","SameSite=Lax"];n&&o.push(`domain=${n}`),typeof location<"u"&&location.protocol==="https:"&&o.push("Secure"),document.cookie=o.join("; ");}var m=class{constructor(e){this.storage=O();this.sessionTimeoutMs=e;let t=N(f)||this.storage?.getItem(f)||null;t||(t=a()),this.anonymousId=t,this.persistAnon(),this.userId=this.storage?.getItem(w)||null;let n=Date.now(),r=this.storage?.getItem(d),o=parseInt(this.storage?.getItem(h)||"0",10);r&&n-o<e?this.sessionId=r:(this.sessionId=a(),this.storage?.setItem(d,this.sessionId)),this.lastActivity=n,this.storage?.setItem(h,String(n));}persistAnon(){this.storage?.setItem(f,this.anonymousId),P(f,this.anonymousId);}touch(){let e=Date.now(),t=e-this.lastActivity>=this.sessionTimeoutMs,n;return t&&(n=this.sessionId,this.sessionId=a(),this.storage?.setItem(d,this.sessionId)),this.lastActivity=e,this.storage?.setItem(h,String(e)),{sessionStarted:t,oldSessionId:n}}setUserId(e){this.userId=e,this.storage?.setItem(w,e);}reset(){this.userId=null,this.storage?.removeItem(w),this.anonymousId=a(),this.persistAnon(),this.sessionId=a(),this.storage?.setItem(d,this.sessionId),this.lastActivity=Date.now(),this.storage?.setItem(h,String(this.lastActivity));}getAnonymousId(){return this.anonymousId}getUserId(){return this.userId}getSessionId(){return this.sessionId}};var p="echo_queue_v1";var g=class{constructor(e){this.buffer=[];this.storage=e==="localstorage"&&typeof localStorage<"u"?localStorage:null,this.restore();}restore(){if(this.storage)try{let e=this.storage.getItem(p);e&&(this.buffer=JSON.parse(e));}catch{this.buffer=[];}}persist(){if(this.storage)try{this.storage.setItem(p,JSON.stringify(this.buffer));}catch{for(;this.buffer.length>50;){this.buffer.shift();try{this.storage.setItem(p,JSON.stringify(this.buffer));break}catch{}}}}enqueue(e){this.buffer.length>=1e3&&this.buffer.shift(),this.buffer.push(e),this.persist();}size(){return this.buffer.length}drain(){let e=this.buffer;return this.buffer=[],this.storage&&this.storage.removeItem(p),e}requeue(e){this.buffer=e.concat(this.buffer).slice(0,1e3),this.persist();}};async function I(i,e){if(i.length===0)return true;let t=e.endpoint.replace(/\/+$/,"")+"/v1/events",n=JSON.stringify({events:i});if(e.useBeacon&&typeof navigator<"u"&&navigator.sendBeacon){let r=`${t}?writeKey=${encodeURIComponent(e.writeKey)}`,o=new Blob([n],{type:"application/json"});return navigator.sendBeacon(r,o)}try{return (await fetch(t,{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":e.writeKey},body:n,keepalive:!0,credentials:"omit"})).ok}catch{return false}}var K="https://events.moso.vn",F=50,H=1e4,B=1800*1e3,y=class{constructor(e){this.flushTimer=null;this.flushing=false;this.currentUrl="";this.previousUrl="";if(!e.writeKey)throw new Error("@mosovn/echo: writeKey is required");this.config={writeKey:e.writeKey,endpoint:e.endpoint??K,flushAt:e.flushAt??F,flushInterval:e.flushInterval??H,sessionTimeoutMs:e.sessionTimeoutMs??B,debug:e.debug??false,...e},this.identity=new m(this.config.sessionTimeoutMs);let t=e.storage==="memory"?"memory":"localstorage";this.queue=new g(t),typeof location<"u"&&(this.currentUrl=location.href),typeof document<"u"&&(this.previousUrl=document.referrer||""),this.startFlushTimer(),this.installPageHideFlush();}markPageview(e){e!==this.currentUrl&&(this.previousUrl=this.currentUrl,this.currentUrl=e);}getPreviousUrl(){return this.previousUrl}debugLog(...e){this.config.debug&&console.log("[echo]",...e);}buildEvent(e,t,n){let{sessionStarted:r,oldSessionId:o}=this.identity.touch();return r&&o&&(this.enqueueRaw({_id:a(),eventName:"$session_end",eventType:"auto",anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:o}),this.enqueueRaw({_id:a(),eventName:"$session_start",eventType:"auto",anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:this.identity.getSessionId()})),{_id:a(),eventName:e,eventType:t,anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:this.identity.getSessionId(),properties:n,context:this.collectContext()}}collectContext(){let e={lib:{name:"@mosovn/echo",version:"0.2.0"}};return typeof location<"u"&&(e.page={url:location.href,path:location.pathname,referrer:this.previousUrl,title:typeof document<"u"?document.title:""}),typeof navigator<"u"&&(e.locale=navigator.language),typeof screen<"u"&&(e.screen={width:screen.width,height:screen.height}),e}enqueueRaw(e){let t=this.config.beforeSend,n=t?t(e):e;n&&(this.queue.enqueue(n),this.debugLog("enqueue",n.eventName,n._id),this.queue.size()>=this.config.flushAt&&this.flush());}enqueueEvent(e,t,n){let r=this.buildEvent(e,t,n);this.enqueueRaw(r);}track(e,t){this.enqueueEvent(e,"custom",t);}identify(e,t){this.identity.setUserId(e),this.enqueueEvent("$identify","auto",t),fetch(this.config.endpoint.replace(/\/+$/,"")+"/v1/identify",{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":this.config.writeKey},body:JSON.stringify({userId:e,anonymousId:this.identity.getAnonymousId(),traits:t||{}}),keepalive:true,credentials:"omit"}).catch(()=>{});}setUserProperties(e){let t=this.identity.getUserId();t?this.identify(t,e):this.enqueueEvent("$set_user_properties","auto",e);}page(e){this.enqueueEvent("$pageview","auto",e);}reset(){this.identity.reset(),this.queue.drain();}async flush(){if(this.flushing||this.queue.size()===0)return;this.flushing=true;let e=this.queue.drain();await I(e,{writeKey:this.config.writeKey,endpoint:this.config.endpoint})?this.debugLog("flush ok",e.length):(this.queue.requeue(e),this.debugLog("flush failed, requeued",e.length)),this.flushing=false;}getAnonymousId(){return this.identity.getAnonymousId()}getUserId(){return this.identity.getUserId()}getSessionId(){return this.identity.getSessionId()}startFlushTimer(){typeof setInterval>"u"||(this.flushTimer=setInterval(()=>{this.flush();},this.config.flushInterval));}installPageHideFlush(){if(typeof document>"u")return;let e=()=>{let t=this.queue.drain();t.length!==0&&I(t,{writeKey:this.config.writeKey,endpoint:this.config.endpoint,useBeacon:true});};document.addEventListener("pagehide",e),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&e();});}};var j=["input","textarea","select","[data-private]"],V=["[data-no-track]","script","style"];function T(i,e){for(let t of e)try{if(i.matches(t))return !0}catch{}return false}function l(i,e){let t=e.blockSelectors&&e.blockSelectors.length?e.blockSelectors:V;return T(i,t)}function Y(i,e){let t=e.maskTextSelectors&&e.maskTextSelectors.length?e.maskTextSelectors:j;return T(i,t)}function J(i,e){return Y(i,e)?"":(i.innerText||i.textContent||"").replace(/\s+/g," ").trim().slice(0,200)}function S(i){let e=[],t=i,n=0;for(;t&&t.nodeType===1&&n<6;){let r=t.tagName.toLowerCase();if(t.id){e.unshift(`${r}#${t.id}`);break}let o=(t.getAttribute("class")||"").split(/\s+/).filter(Boolean).slice(0,2).map(u=>"."+u).join(""),s="";if(t.parentElement){let u=Array.from(t.parentElement.children).filter(c=>c.tagName===t.tagName);u.length>1&&(s=`:nth-of-type(${u.indexOf(t)+1})`);}e.unshift(`${r}${o}${s}`),t=t.parentElement,n++;}return e.join(" > ")}function E(i,e){let t=i.tagName.toLowerCase(),n={selector:S(i),tag:t,text:J(i,e)},r=i.id;r&&(n.id=r);let o=i.getAttribute&&i.getAttribute("name");if(o&&(n.name=o),t==="a"){let u=i.href;u&&(n.href=u);}let s=i.getAttribute&&i.getAttribute("aria-label");return s&&(n.ariaLabel=s),n}function v(i){let e=i.tagName?.toLowerCase();if(!e)return false;if(e==="a"||e==="button")return true;let t=i.getAttribute&&i.getAttribute("role");return t==="button"||t==="link"||t==="tab"||t==="menuitem"}function _(i,e,t){typeof document>"u"||document.addEventListener("click",n=>{let r=n.target;if(r){if(e==="instrumented"){let o=r;for(;o&&o!==document.body&&!v(o);)o=o.parentElement;if(!o||o===document.body)return;r=o;}l(r,t)||i.enqueueEvent("$click","auto",E(r,t));}},true);}function k(i){if(typeof window>"u")return;let e=[],t=n=>{let r=Date.now();for(;e.length&&r-e[0].at>5e3;)e.shift();return e.some(o=>o.key===n)?false:(e.push({key:n,at:r}),true)};window.addEventListener("error",n=>{let r=n.message||String(n.error||"unknown"),o=`${r}|${n.filename}|${n.lineno}`;t(o)&&i.enqueueEvent("$error","auto",{message:r,source:n.filename,line:n.lineno,column:n.colno,stack:n.error&&n.error.stack||void 0});}),window.addEventListener("unhandledrejection",n=>{let r=n.reason,o=r instanceof Error?r.message:String(r),s=r instanceof Error?r.stack:void 0,u=`unhandled:${o}`;t(u)&&i.enqueueEvent("$error","auto",{kind:"unhandledrejection",message:o,stack:s});});}function A(i,e){typeof document>"u"||document.addEventListener("submit",t=>{let n=t.target;if(!n||n.tagName!=="FORM"||l(n,e))return;let r=n.querySelectorAll("input, select, textarea").length;i.enqueueEvent("$form_submit","auto",{selector:S(n),id:n.id||void 0,name:n.getAttribute("name")||void 0,action:n.getAttribute("action")||void 0,method:(n.getAttribute("method")||"get").toUpperCase(),fieldCount:r});},true);}var X=500;function x(i,e){if(typeof document>"u")return;let t=null,n=0;document.addEventListener("mouseover",r=>{let o=r.target;if(!o)return;let s=o;for(;s&&s!==document.body&&!v(s);)s=s.parentElement;if(!s||s===document.body||s===t||l(s,e))return;let u=Date.now();if(u-n<X){t=s;return}t=s,n=u,i.enqueueEvent("$hover","auto",E(s,e));},true);}function R(i){if(typeof window>"u"||typeof history>"u")return;let e=location.href,t=0,n=()=>{let o=Date.now();o-t<100||location.href===e&&t!==0||(e=location.href,t=o,i.markPageview(location.href),i.enqueueEvent("$pageview","auto",{url:location.href,path:location.pathname,title:document?.title,referrer:i.getPreviousUrl()}));};n();let r=o=>{let s=history[o];history[o]=function(...u){let c=s.apply(history,u);return n(),c};};r("pushState"),r("replaceState"),window.addEventListener("popstate",n),window.addEventListener("hashchange",n);}function U(i){if(typeof window>"u"||typeof document>"u")return;let e=0,t=0,n=location.href,r=()=>{location.href!==n&&(n=location.href,e=0,t=0);let o=Date.now();if(o-t<250)return;let s=document.documentElement,u=window.scrollY||s.scrollTop||0,c=window.innerHeight||s.clientHeight,C=s.scrollHeight-c;if(C<=0)return;let b=Math.max(0,Math.min(100,Math.round(u/C*100)));b-e<10||(e=b,t=o,i.enqueueEvent("$scroll","auto",{percent:b,pixels:u}));};window.addEventListener("scroll",r,{passive:true});}function L(i){if(typeof document>"u")return;let e=document.visibilityState;document.addEventListener("visibilitychange",()=>{let t=document.visibilityState;t!==e&&(e=t,i.enqueueEvent("$visibility","auto",{state:t}));});}function M(i){if(typeof PerformanceObserver>"u")return;let e=(t,n,r)=>{i.enqueueEvent("$web_vital","auto",{name:t,value:n,...r});};try{new PerformanceObserver(n=>{let r=n.getEntries(),o=r[r.length-1];o&&e("LCP",Math.round(o.startTime));}).observe({type:"largest-contentful-paint",buffered:!0});}catch{}try{let t=0;new PerformanceObserver(r=>{for(let o of r.getEntries())o.hadRecentInput||(t+=o.value);}).observe({type:"layout-shift",buffered:!0}),addEventListener("pagehide",()=>e("CLS",Math.round(t*1e3)/1e3));}catch{}try{new PerformanceObserver(n=>{let r=n.getEntries()[0];r&&e("FID",Math.round(r.processingStart-r.startTime));}).observe({type:"first-input",buffered:!0});}catch{}}var q={pageviews:true,clicks:"all",forms:true,scroll:true,visibility:true,mouseover:true,errors:true,vitals:true,sessions:true};function Q(i){return i===false?null:i===void 0||i===true?q:{...q,...i}}function $(i,e){let t=Q(e.autoCapture);if(!t)return;let n={maskTextSelectors:e.maskTextSelectors,blockSelectors:e.blockSelectors};if(t.pageviews&&R(i),t.clicks){let r=t.clicks==="instrumented"?"instrumented":"all";_(i,r,n);}t.forms&&A(i,n),t.scroll&&U(i),t.visibility&&L(i),t.mouseover&&x(i,n),t.errors&&k(i),t.vitals&&M(i);}function xe(i){let e=new y(i);return $(e,i),e}
2
2
  exports.createEcho=xe;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Shared types for both `@mosovn/echo` entries (web + native).
3
+ * Platform-specific options are tagged in JSDoc; setting a web-only option in
4
+ * RN (or vice versa) is silently ignored, not an error.
5
+ */
1
6
  interface EchoConfig {
2
7
  /** Project's public write key (e.g. `pk_xxx`). */
3
8
  writeKey: string;
@@ -9,25 +14,32 @@ interface EchoConfig {
9
14
  flushInterval?: number;
10
15
  /** Optional middleware called on every event before queueing. Return `null` to drop. */
11
16
  beforeSend?: (event: EchoEvent) => EchoEvent | null;
12
- /** Where to persist the queue between page loads. Default 'indexeddb'. */
13
- storage?: 'indexeddb' | 'localstorage' | 'memory';
14
- /** Autocapture configuration. See AutoCaptureConfig. */
15
- autoCapture?: AutoCaptureConfig | boolean;
16
- /**
17
- * CSS selectors whose text content should be masked when captured (e.g. for
18
- * autocaptured click events on inputs). Defaults cover form fields.
19
- */
20
- maskTextSelectors?: string[];
21
- /** CSS selectors to fully skip during autocapture. */
22
- blockSelectors?: string[];
23
17
  /** Inactivity timeout (ms) to end a session. Default 30 min. */
24
18
  sessionTimeoutMs?: number;
25
19
  /** If true, log internal diagnostic info to console. */
26
20
  debug?: boolean;
21
+ /** [Web only] Where to persist the queue. Default 'localstorage'. */
22
+ storage?: 'indexeddb' | 'localstorage' | 'memory';
23
+ /** [Web only] Autocapture configuration (DOM-based). See AutoCaptureConfig. */
24
+ autoCapture?: AutoCaptureConfig | boolean;
25
+ /** [Web only] CSS selectors whose text content is masked from captured events. */
26
+ maskTextSelectors?: string[];
27
+ /** [Web only] CSS selectors to skip during autocapture. */
28
+ blockSelectors?: string[];
29
+ /** [Native only] App info attached to event context. */
30
+ app?: {
31
+ name?: string;
32
+ version?: string;
33
+ build?: string;
34
+ };
35
+ /** [Native only] Track AppState transitions as $session_start/end. Default true. */
36
+ trackSessions?: boolean;
37
+ /** [Native only] Track JS errors via ErrorUtils + unhandledrejection. Default true. */
38
+ trackErrors?: boolean;
27
39
  }
28
40
  interface AutoCaptureConfig {
29
41
  pageviews?: boolean;
30
- /** 'instrumented' = a/button/[data-track]/[role=button]. 'all' = every click. */
42
+ /** 'instrumented' = a/button/[role=button]/[id]. 'all' = every click. */
31
43
  clicks?: boolean | 'instrumented' | 'all';
32
44
  forms?: boolean;
33
45
  scroll?: boolean;
@@ -50,11 +62,19 @@ interface EchoEvent {
50
62
  context?: Record<string, unknown>;
51
63
  }
52
64
  interface EchoClient {
65
+ /** Track a custom event. */
53
66
  track(eventName: string, properties?: Record<string, unknown>): void;
67
+ /** Identify the current user. Call after login. */
54
68
  identify(userId: string, traits?: Record<string, unknown>): void;
69
+ /** Update user traits. */
55
70
  setUserProperties(traits: Record<string, unknown>): void;
56
- page(properties?: Record<string, unknown>): void;
57
- reset(): void;
71
+ /** [Web only] Record a page view (URL-based). No-op on native. */
72
+ page?(properties?: Record<string, unknown>): void;
73
+ /** [Native only] Record a screen view. No-op on web. */
74
+ screen?(screenName: string, properties?: Record<string, unknown>): void;
75
+ /** Clear identity (call on logout). Rotates anonymousId. */
76
+ reset(): void | Promise<void>;
77
+ /** Force flush queued events now. */
58
78
  flush(): Promise<void>;
59
79
  getAnonymousId(): string;
60
80
  getUserId(): string | null;
@@ -62,7 +82,10 @@ interface EchoClient {
62
82
  }
63
83
 
64
84
  /**
65
- * @mosovn/echo — Echo web SDK.
85
+ * @mosovn/echo — web entry.
86
+ *
87
+ * Bundlers resolve this entry via the `browser` / default condition in
88
+ * `package.json` exports. React Native (Metro) loads `./native.ts` instead.
66
89
  *
67
90
  * Usage:
68
91
  *
@@ -71,9 +94,6 @@ interface EchoClient {
71
94
  * const echo = createEcho({ writeKey: 'pk_xxx' });
72
95
  * echo.track('Sign Up', { plan: 'pro' });
73
96
  * echo.identify('user_123', { email: 'foo@moso.vn' });
74
- *
75
- * Functional API — no module-level globals. You can create multiple clients
76
- * targeting different projects (e.g. for multi-tenant test apps).
77
97
  */
78
98
 
79
99
  declare function createEcho(config: EchoConfig): EchoClient;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Shared types for both `@mosovn/echo` entries (web + native).
3
+ * Platform-specific options are tagged in JSDoc; setting a web-only option in
4
+ * RN (or vice versa) is silently ignored, not an error.
5
+ */
1
6
  interface EchoConfig {
2
7
  /** Project's public write key (e.g. `pk_xxx`). */
3
8
  writeKey: string;
@@ -9,25 +14,32 @@ interface EchoConfig {
9
14
  flushInterval?: number;
10
15
  /** Optional middleware called on every event before queueing. Return `null` to drop. */
11
16
  beforeSend?: (event: EchoEvent) => EchoEvent | null;
12
- /** Where to persist the queue between page loads. Default 'indexeddb'. */
13
- storage?: 'indexeddb' | 'localstorage' | 'memory';
14
- /** Autocapture configuration. See AutoCaptureConfig. */
15
- autoCapture?: AutoCaptureConfig | boolean;
16
- /**
17
- * CSS selectors whose text content should be masked when captured (e.g. for
18
- * autocaptured click events on inputs). Defaults cover form fields.
19
- */
20
- maskTextSelectors?: string[];
21
- /** CSS selectors to fully skip during autocapture. */
22
- blockSelectors?: string[];
23
17
  /** Inactivity timeout (ms) to end a session. Default 30 min. */
24
18
  sessionTimeoutMs?: number;
25
19
  /** If true, log internal diagnostic info to console. */
26
20
  debug?: boolean;
21
+ /** [Web only] Where to persist the queue. Default 'localstorage'. */
22
+ storage?: 'indexeddb' | 'localstorage' | 'memory';
23
+ /** [Web only] Autocapture configuration (DOM-based). See AutoCaptureConfig. */
24
+ autoCapture?: AutoCaptureConfig | boolean;
25
+ /** [Web only] CSS selectors whose text content is masked from captured events. */
26
+ maskTextSelectors?: string[];
27
+ /** [Web only] CSS selectors to skip during autocapture. */
28
+ blockSelectors?: string[];
29
+ /** [Native only] App info attached to event context. */
30
+ app?: {
31
+ name?: string;
32
+ version?: string;
33
+ build?: string;
34
+ };
35
+ /** [Native only] Track AppState transitions as $session_start/end. Default true. */
36
+ trackSessions?: boolean;
37
+ /** [Native only] Track JS errors via ErrorUtils + unhandledrejection. Default true. */
38
+ trackErrors?: boolean;
27
39
  }
28
40
  interface AutoCaptureConfig {
29
41
  pageviews?: boolean;
30
- /** 'instrumented' = a/button/[data-track]/[role=button]. 'all' = every click. */
42
+ /** 'instrumented' = a/button/[role=button]/[id]. 'all' = every click. */
31
43
  clicks?: boolean | 'instrumented' | 'all';
32
44
  forms?: boolean;
33
45
  scroll?: boolean;
@@ -50,11 +62,19 @@ interface EchoEvent {
50
62
  context?: Record<string, unknown>;
51
63
  }
52
64
  interface EchoClient {
65
+ /** Track a custom event. */
53
66
  track(eventName: string, properties?: Record<string, unknown>): void;
67
+ /** Identify the current user. Call after login. */
54
68
  identify(userId: string, traits?: Record<string, unknown>): void;
69
+ /** Update user traits. */
55
70
  setUserProperties(traits: Record<string, unknown>): void;
56
- page(properties?: Record<string, unknown>): void;
57
- reset(): void;
71
+ /** [Web only] Record a page view (URL-based). No-op on native. */
72
+ page?(properties?: Record<string, unknown>): void;
73
+ /** [Native only] Record a screen view. No-op on web. */
74
+ screen?(screenName: string, properties?: Record<string, unknown>): void;
75
+ /** Clear identity (call on logout). Rotates anonymousId. */
76
+ reset(): void | Promise<void>;
77
+ /** Force flush queued events now. */
58
78
  flush(): Promise<void>;
59
79
  getAnonymousId(): string;
60
80
  getUserId(): string | null;
@@ -62,7 +82,10 @@ interface EchoClient {
62
82
  }
63
83
 
64
84
  /**
65
- * @mosovn/echo — Echo web SDK.
85
+ * @mosovn/echo — web entry.
86
+ *
87
+ * Bundlers resolve this entry via the `browser` / default condition in
88
+ * `package.json` exports. React Native (Metro) loads `./native.ts` instead.
66
89
  *
67
90
  * Usage:
68
91
  *
@@ -71,9 +94,6 @@ interface EchoClient {
71
94
  * const echo = createEcho({ writeKey: 'pk_xxx' });
72
95
  * echo.track('Sign Up', { plan: 'pro' });
73
96
  * echo.identify('user_123', { email: 'foo@moso.vn' });
74
- *
75
- * Functional API — no module-level globals. You can create multiple clients
76
- * targeting different projects (e.g. for multi-tenant test apps).
77
97
  */
78
98
 
79
99
  declare function createEcho(config: EchoConfig): EchoClient;
@@ -1,2 +1,2 @@
1
- function a(){let i=BigInt(Date.now()),e=crypto.getRandomValues(new Uint8Array(10)),t=new Uint8Array(16);t[0]=Number(i>>40n&0xffn),t[1]=Number(i>>32n&0xffn),t[2]=Number(i>>24n&0xffn),t[3]=Number(i>>16n&0xffn),t[4]=Number(i>>8n&0xffn),t[5]=Number(i&0xffn),t[6]=e[0]&15|112,t[7]=e[1],t[8]=e[2]&63|128,t[9]=e[3],t[10]=e[4],t[11]=e[5],t[12]=e[6],t[13]=e[7],t[14]=e[8],t[15]=e[9];let n=Array.from(t,r=>r.toString(16).padStart(2,"0")).join("");return `${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`}var f="echo_anonymous_id",w="echo_user_id",d="echo_session_id",h="echo_last_activity";function O(){try{return typeof localStorage>"u"?null:(localStorage.setItem("__echo_probe__","1"),localStorage.removeItem("__echo_probe__"),localStorage)}catch{return null}}function D(){if(typeof location>"u")return null;let i=location.hostname;if(i==="localhost"||/^\d+\.\d+\.\d+\.\d+$/.test(i))return null;let e=i.split(".");return e.length<2?null:"."+e.slice(-2).join(".")}function N(i){if(typeof document>"u")return null;let e=document.cookie.match(new RegExp("(?:^|; )"+i+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}function P(i,e,t=365){if(typeof document>"u")return;let n=D(),r=new Date(Date.now()+t*864e5).toUTCString(),o=[`${i}=${encodeURIComponent(e)}`,`expires=${r}`,"path=/","SameSite=Lax"];n&&o.push(`domain=${n}`),typeof location<"u"&&location.protocol==="https:"&&o.push("Secure"),document.cookie=o.join("; ");}var m=class{constructor(e){this.storage=O();this.sessionTimeoutMs=e;let t=N(f)||this.storage?.getItem(f)||null;t||(t=a()),this.anonymousId=t,this.persistAnon(),this.userId=this.storage?.getItem(w)||null;let n=Date.now(),r=this.storage?.getItem(d),o=parseInt(this.storage?.getItem(h)||"0",10);r&&n-o<e?this.sessionId=r:(this.sessionId=a(),this.storage?.setItem(d,this.sessionId)),this.lastActivity=n,this.storage?.setItem(h,String(n));}persistAnon(){this.storage?.setItem(f,this.anonymousId),P(f,this.anonymousId);}touch(){let e=Date.now(),t=e-this.lastActivity>=this.sessionTimeoutMs,n;return t&&(n=this.sessionId,this.sessionId=a(),this.storage?.setItem(d,this.sessionId)),this.lastActivity=e,this.storage?.setItem(h,String(e)),{sessionStarted:t,oldSessionId:n}}setUserId(e){this.userId=e,this.storage?.setItem(w,e);}reset(){this.userId=null,this.storage?.removeItem(w),this.anonymousId=a(),this.persistAnon(),this.sessionId=a(),this.storage?.setItem(d,this.sessionId),this.lastActivity=Date.now(),this.storage?.setItem(h,String(this.lastActivity));}getAnonymousId(){return this.anonymousId}getUserId(){return this.userId}getSessionId(){return this.sessionId}};var p="echo_queue_v1";var g=class{constructor(e){this.buffer=[];this.storage=e==="localstorage"&&typeof localStorage<"u"?localStorage:null,this.restore();}restore(){if(this.storage)try{let e=this.storage.getItem(p);e&&(this.buffer=JSON.parse(e));}catch{this.buffer=[];}}persist(){if(this.storage)try{this.storage.setItem(p,JSON.stringify(this.buffer));}catch{for(;this.buffer.length>50;){this.buffer.shift();try{this.storage.setItem(p,JSON.stringify(this.buffer));break}catch{}}}}enqueue(e){this.buffer.length>=1e3&&this.buffer.shift(),this.buffer.push(e),this.persist();}size(){return this.buffer.length}drain(){let e=this.buffer;return this.buffer=[],this.storage&&this.storage.removeItem(p),e}requeue(e){this.buffer=e.concat(this.buffer).slice(0,1e3),this.persist();}};async function I(i,e){if(i.length===0)return true;let t=e.endpoint.replace(/\/+$/,"")+"/v1/events",n=JSON.stringify({events:i});if(e.useBeacon&&typeof navigator<"u"&&navigator.sendBeacon){let r=`${t}?writeKey=${encodeURIComponent(e.writeKey)}`,o=new Blob([n],{type:"application/json"});return navigator.sendBeacon(r,o)}try{return (await fetch(t,{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":e.writeKey},body:n,keepalive:!0,credentials:"omit"})).ok}catch{return false}}var K="https://events.moso.vn",F=50,H=1e4,B=1800*1e3,y=class{constructor(e){this.flushTimer=null;this.flushing=false;this.currentUrl="";this.previousUrl="";if(!e.writeKey)throw new Error("@mosovn/echo: writeKey is required");this.config={writeKey:e.writeKey,endpoint:e.endpoint??K,flushAt:e.flushAt??F,flushInterval:e.flushInterval??H,sessionTimeoutMs:e.sessionTimeoutMs??B,debug:e.debug??false,...e},this.identity=new m(this.config.sessionTimeoutMs);let t=e.storage==="memory"?"memory":"localstorage";this.queue=new g(t),typeof location<"u"&&(this.currentUrl=location.href),typeof document<"u"&&(this.previousUrl=document.referrer||""),this.startFlushTimer(),this.installPageHideFlush();}markPageview(e){e!==this.currentUrl&&(this.previousUrl=this.currentUrl,this.currentUrl=e);}getPreviousUrl(){return this.previousUrl}debugLog(...e){this.config.debug&&console.log("[echo]",...e);}buildEvent(e,t,n){let{sessionStarted:r,oldSessionId:o}=this.identity.touch();return r&&o&&(this.enqueueRaw({_id:a(),eventName:"$session_end",eventType:"auto",anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:o}),this.enqueueRaw({_id:a(),eventName:"$session_start",eventType:"auto",anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:this.identity.getSessionId()})),{_id:a(),eventName:e,eventType:t,anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:this.identity.getSessionId(),properties:n,context:this.collectContext()}}collectContext(){let e={lib:{name:"@mosovn/echo",version:"0.0.0"}};return typeof location<"u"&&(e.page={url:location.href,path:location.pathname,referrer:this.previousUrl,title:typeof document<"u"?document.title:""}),typeof navigator<"u"&&(e.locale=navigator.language),typeof screen<"u"&&(e.screen={width:screen.width,height:screen.height}),e}enqueueRaw(e){let t=this.config.beforeSend,n=t?t(e):e;n&&(this.queue.enqueue(n),this.debugLog("enqueue",n.eventName,n._id),this.queue.size()>=this.config.flushAt&&this.flush());}enqueueEvent(e,t,n){let r=this.buildEvent(e,t,n);this.enqueueRaw(r);}track(e,t){this.enqueueEvent(e,"custom",t);}identify(e,t){this.identity.setUserId(e),this.enqueueEvent("$identify","auto",t),fetch(this.config.endpoint.replace(/\/+$/,"")+"/v1/identify",{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":this.config.writeKey},body:JSON.stringify({userId:e,anonymousId:this.identity.getAnonymousId(),traits:t||{}}),keepalive:true,credentials:"omit"}).catch(()=>{});}setUserProperties(e){let t=this.identity.getUserId();t?this.identify(t,e):this.enqueueEvent("$set_user_properties","auto",e);}page(e){this.enqueueEvent("$pageview","auto",e);}reset(){this.identity.reset(),this.queue.drain();}async flush(){if(this.flushing||this.queue.size()===0)return;this.flushing=true;let e=this.queue.drain();await I(e,{writeKey:this.config.writeKey,endpoint:this.config.endpoint})?this.debugLog("flush ok",e.length):(this.queue.requeue(e),this.debugLog("flush failed, requeued",e.length)),this.flushing=false;}getAnonymousId(){return this.identity.getAnonymousId()}getUserId(){return this.identity.getUserId()}getSessionId(){return this.identity.getSessionId()}startFlushTimer(){typeof setInterval>"u"||(this.flushTimer=setInterval(()=>{this.flush();},this.config.flushInterval));}installPageHideFlush(){if(typeof document>"u")return;let e=()=>{let t=this.queue.drain();t.length!==0&&I(t,{writeKey:this.config.writeKey,endpoint:this.config.endpoint,useBeacon:true});};document.addEventListener("pagehide",e),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&e();});}};var j=["input","textarea","select","[data-private]"],V=["[data-no-track]","script","style"];function T(i,e){for(let t of e)try{if(i.matches(t))return !0}catch{}return false}function l(i,e){let t=e.blockSelectors&&e.blockSelectors.length?e.blockSelectors:V;return T(i,t)}function Y(i,e){let t=e.maskTextSelectors&&e.maskTextSelectors.length?e.maskTextSelectors:j;return T(i,t)}function J(i,e){return Y(i,e)?"":(i.innerText||i.textContent||"").replace(/\s+/g," ").trim().slice(0,200)}function S(i){let e=[],t=i,n=0;for(;t&&t.nodeType===1&&n<6;){let r=t.tagName.toLowerCase();if(t.id){e.unshift(`${r}#${t.id}`);break}let o=(t.getAttribute("class")||"").split(/\s+/).filter(Boolean).slice(0,2).map(u=>"."+u).join(""),s="";if(t.parentElement){let u=Array.from(t.parentElement.children).filter(c=>c.tagName===t.tagName);u.length>1&&(s=`:nth-of-type(${u.indexOf(t)+1})`);}e.unshift(`${r}${o}${s}`),t=t.parentElement,n++;}return e.join(" > ")}function E(i,e){let t=i.tagName.toLowerCase(),n={selector:S(i),tag:t,text:J(i,e)},r=i.id;r&&(n.id=r);let o=i.getAttribute&&i.getAttribute("name");if(o&&(n.name=o),t==="a"){let u=i.href;u&&(n.href=u);}let s=i.getAttribute&&i.getAttribute("aria-label");return s&&(n.ariaLabel=s),n}function v(i){let e=i.tagName?.toLowerCase();if(!e)return false;if(e==="a"||e==="button")return true;let t=i.getAttribute&&i.getAttribute("role");return t==="button"||t==="link"||t==="tab"||t==="menuitem"}function k(i,e,t){typeof document>"u"||document.addEventListener("click",n=>{let r=n.target;if(r){if(e==="instrumented"){let o=r;for(;o&&o!==document.body&&!v(o);)o=o.parentElement;if(!o||o===document.body)return;r=o;}l(r,t)||i.enqueueEvent("$click","auto",E(r,t));}},true);}function A(i){if(typeof window>"u")return;let e=[],t=n=>{let r=Date.now();for(;e.length&&r-e[0].at>5e3;)e.shift();return e.some(o=>o.key===n)?false:(e.push({key:n,at:r}),true)};window.addEventListener("error",n=>{let r=n.message||String(n.error||"unknown"),o=`${r}|${n.filename}|${n.lineno}`;t(o)&&i.enqueueEvent("$error","auto",{message:r,source:n.filename,line:n.lineno,column:n.colno,stack:n.error&&n.error.stack||void 0});}),window.addEventListener("unhandledrejection",n=>{let r=n.reason,o=r instanceof Error?r.message:String(r),s=r instanceof Error?r.stack:void 0,u=`unhandled:${o}`;t(u)&&i.enqueueEvent("$error","auto",{kind:"unhandledrejection",message:o,stack:s});});}function _(i,e){typeof document>"u"||document.addEventListener("submit",t=>{let n=t.target;if(!n||n.tagName!=="FORM"||l(n,e))return;let r=n.querySelectorAll("input, select, textarea").length;i.enqueueEvent("$form_submit","auto",{selector:S(n),id:n.id||void 0,name:n.getAttribute("name")||void 0,action:n.getAttribute("action")||void 0,method:(n.getAttribute("method")||"get").toUpperCase(),fieldCount:r});},true);}var X=500;function x(i,e){if(typeof document>"u")return;let t=null,n=0;document.addEventListener("mouseover",r=>{let o=r.target;if(!o)return;let s=o;for(;s&&s!==document.body&&!v(s);)s=s.parentElement;if(!s||s===document.body||s===t||l(s,e))return;let u=Date.now();if(u-n<X){t=s;return}t=s,n=u,i.enqueueEvent("$hover","auto",E(s,e));},true);}function R(i){if(typeof window>"u"||typeof history>"u")return;let e=location.href,t=0,n=()=>{let o=Date.now();o-t<100||location.href===e&&t!==0||(e=location.href,t=o,i.markPageview(location.href),i.enqueueEvent("$pageview","auto",{url:location.href,path:location.pathname,title:document?.title,referrer:i.getPreviousUrl()}));};n();let r=o=>{let s=history[o];history[o]=function(...u){let c=s.apply(history,u);return n(),c};};r("pushState"),r("replaceState"),window.addEventListener("popstate",n),window.addEventListener("hashchange",n);}function U(i){if(typeof window>"u"||typeof document>"u")return;let e=0,t=0,n=location.href,r=()=>{location.href!==n&&(n=location.href,e=0,t=0);let o=Date.now();if(o-t<250)return;let s=document.documentElement,u=window.scrollY||s.scrollTop||0,c=window.innerHeight||s.clientHeight,C=s.scrollHeight-c;if(C<=0)return;let b=Math.max(0,Math.min(100,Math.round(u/C*100)));b-e<10||(e=b,t=o,i.enqueueEvent("$scroll","auto",{percent:b,pixels:u}));};window.addEventListener("scroll",r,{passive:true});}function L(i){if(typeof document>"u")return;let e=document.visibilityState;document.addEventListener("visibilitychange",()=>{let t=document.visibilityState;t!==e&&(e=t,i.enqueueEvent("$visibility","auto",{state:t}));});}function M(i){if(typeof PerformanceObserver>"u")return;let e=(t,n,r)=>{i.enqueueEvent("$web_vital","auto",{name:t,value:n,...r});};try{new PerformanceObserver(n=>{let r=n.getEntries(),o=r[r.length-1];o&&e("LCP",Math.round(o.startTime));}).observe({type:"largest-contentful-paint",buffered:!0});}catch{}try{let t=0;new PerformanceObserver(r=>{for(let o of r.getEntries())o.hadRecentInput||(t+=o.value);}).observe({type:"layout-shift",buffered:!0}),addEventListener("pagehide",()=>e("CLS",Math.round(t*1e3)/1e3));}catch{}try{new PerformanceObserver(n=>{let r=n.getEntries()[0];r&&e("FID",Math.round(r.processingStart-r.startTime));}).observe({type:"first-input",buffered:!0});}catch{}}var q={pageviews:true,clicks:"all",forms:true,scroll:true,visibility:true,mouseover:true,errors:true,vitals:true,sessions:true};function Q(i){return i===false?null:i===void 0||i===true?q:{...q,...i}}function $(i,e){let t=Q(e.autoCapture);if(!t)return;let n={maskTextSelectors:e.maskTextSelectors,blockSelectors:e.blockSelectors};if(t.pageviews&&R(i),t.clicks){let r=t.clicks==="instrumented"?"instrumented":"all";k(i,r,n);}t.forms&&_(i,n),t.scroll&&U(i),t.visibility&&L(i),t.mouseover&&x(i,n),t.errors&&A(i),t.vitals&&M(i);}function xe(i){let e=new y(i);return $(e,i),e}
1
+ function a(){let i=BigInt(Date.now()),e=crypto.getRandomValues(new Uint8Array(10)),t=new Uint8Array(16);t[0]=Number(i>>40n&0xffn),t[1]=Number(i>>32n&0xffn),t[2]=Number(i>>24n&0xffn),t[3]=Number(i>>16n&0xffn),t[4]=Number(i>>8n&0xffn),t[5]=Number(i&0xffn),t[6]=e[0]&15|112,t[7]=e[1],t[8]=e[2]&63|128,t[9]=e[3],t[10]=e[4],t[11]=e[5],t[12]=e[6],t[13]=e[7],t[14]=e[8],t[15]=e[9];let n=Array.from(t,r=>r.toString(16).padStart(2,"0")).join("");return `${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`}var f="echo_anonymous_id",w="echo_user_id",d="echo_session_id",h="echo_last_activity";function O(){try{return typeof localStorage>"u"?null:(localStorage.setItem("__echo_probe__","1"),localStorage.removeItem("__echo_probe__"),localStorage)}catch{return null}}function D(){if(typeof location>"u")return null;let i=location.hostname;if(i==="localhost"||/^\d+\.\d+\.\d+\.\d+$/.test(i))return null;let e=i.split(".");return e.length<2?null:"."+e.slice(-2).join(".")}function N(i){if(typeof document>"u")return null;let e=document.cookie.match(new RegExp("(?:^|; )"+i+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}function P(i,e,t=365){if(typeof document>"u")return;let n=D(),r=new Date(Date.now()+t*864e5).toUTCString(),o=[`${i}=${encodeURIComponent(e)}`,`expires=${r}`,"path=/","SameSite=Lax"];n&&o.push(`domain=${n}`),typeof location<"u"&&location.protocol==="https:"&&o.push("Secure"),document.cookie=o.join("; ");}var m=class{constructor(e){this.storage=O();this.sessionTimeoutMs=e;let t=N(f)||this.storage?.getItem(f)||null;t||(t=a()),this.anonymousId=t,this.persistAnon(),this.userId=this.storage?.getItem(w)||null;let n=Date.now(),r=this.storage?.getItem(d),o=parseInt(this.storage?.getItem(h)||"0",10);r&&n-o<e?this.sessionId=r:(this.sessionId=a(),this.storage?.setItem(d,this.sessionId)),this.lastActivity=n,this.storage?.setItem(h,String(n));}persistAnon(){this.storage?.setItem(f,this.anonymousId),P(f,this.anonymousId);}touch(){let e=Date.now(),t=e-this.lastActivity>=this.sessionTimeoutMs,n;return t&&(n=this.sessionId,this.sessionId=a(),this.storage?.setItem(d,this.sessionId)),this.lastActivity=e,this.storage?.setItem(h,String(e)),{sessionStarted:t,oldSessionId:n}}setUserId(e){this.userId=e,this.storage?.setItem(w,e);}reset(){this.userId=null,this.storage?.removeItem(w),this.anonymousId=a(),this.persistAnon(),this.sessionId=a(),this.storage?.setItem(d,this.sessionId),this.lastActivity=Date.now(),this.storage?.setItem(h,String(this.lastActivity));}getAnonymousId(){return this.anonymousId}getUserId(){return this.userId}getSessionId(){return this.sessionId}};var p="echo_queue_v1";var g=class{constructor(e){this.buffer=[];this.storage=e==="localstorage"&&typeof localStorage<"u"?localStorage:null,this.restore();}restore(){if(this.storage)try{let e=this.storage.getItem(p);e&&(this.buffer=JSON.parse(e));}catch{this.buffer=[];}}persist(){if(this.storage)try{this.storage.setItem(p,JSON.stringify(this.buffer));}catch{for(;this.buffer.length>50;){this.buffer.shift();try{this.storage.setItem(p,JSON.stringify(this.buffer));break}catch{}}}}enqueue(e){this.buffer.length>=1e3&&this.buffer.shift(),this.buffer.push(e),this.persist();}size(){return this.buffer.length}drain(){let e=this.buffer;return this.buffer=[],this.storage&&this.storage.removeItem(p),e}requeue(e){this.buffer=e.concat(this.buffer).slice(0,1e3),this.persist();}};async function I(i,e){if(i.length===0)return true;let t=e.endpoint.replace(/\/+$/,"")+"/v1/events",n=JSON.stringify({events:i});if(e.useBeacon&&typeof navigator<"u"&&navigator.sendBeacon){let r=`${t}?writeKey=${encodeURIComponent(e.writeKey)}`,o=new Blob([n],{type:"application/json"});return navigator.sendBeacon(r,o)}try{return (await fetch(t,{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":e.writeKey},body:n,keepalive:!0,credentials:"omit"})).ok}catch{return false}}var K="https://events.moso.vn",F=50,H=1e4,B=1800*1e3,y=class{constructor(e){this.flushTimer=null;this.flushing=false;this.currentUrl="";this.previousUrl="";if(!e.writeKey)throw new Error("@mosovn/echo: writeKey is required");this.config={writeKey:e.writeKey,endpoint:e.endpoint??K,flushAt:e.flushAt??F,flushInterval:e.flushInterval??H,sessionTimeoutMs:e.sessionTimeoutMs??B,debug:e.debug??false,...e},this.identity=new m(this.config.sessionTimeoutMs);let t=e.storage==="memory"?"memory":"localstorage";this.queue=new g(t),typeof location<"u"&&(this.currentUrl=location.href),typeof document<"u"&&(this.previousUrl=document.referrer||""),this.startFlushTimer(),this.installPageHideFlush();}markPageview(e){e!==this.currentUrl&&(this.previousUrl=this.currentUrl,this.currentUrl=e);}getPreviousUrl(){return this.previousUrl}debugLog(...e){this.config.debug&&console.log("[echo]",...e);}buildEvent(e,t,n){let{sessionStarted:r,oldSessionId:o}=this.identity.touch();return r&&o&&(this.enqueueRaw({_id:a(),eventName:"$session_end",eventType:"auto",anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:o}),this.enqueueRaw({_id:a(),eventName:"$session_start",eventType:"auto",anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:this.identity.getSessionId()})),{_id:a(),eventName:e,eventType:t,anonymousId:this.identity.getAnonymousId(),userId:this.identity.getUserId(),timestamp:new Date().toISOString(),sessionId:this.identity.getSessionId(),properties:n,context:this.collectContext()}}collectContext(){let e={lib:{name:"@mosovn/echo",version:"0.2.0"}};return typeof location<"u"&&(e.page={url:location.href,path:location.pathname,referrer:this.previousUrl,title:typeof document<"u"?document.title:""}),typeof navigator<"u"&&(e.locale=navigator.language),typeof screen<"u"&&(e.screen={width:screen.width,height:screen.height}),e}enqueueRaw(e){let t=this.config.beforeSend,n=t?t(e):e;n&&(this.queue.enqueue(n),this.debugLog("enqueue",n.eventName,n._id),this.queue.size()>=this.config.flushAt&&this.flush());}enqueueEvent(e,t,n){let r=this.buildEvent(e,t,n);this.enqueueRaw(r);}track(e,t){this.enqueueEvent(e,"custom",t);}identify(e,t){this.identity.setUserId(e),this.enqueueEvent("$identify","auto",t),fetch(this.config.endpoint.replace(/\/+$/,"")+"/v1/identify",{method:"POST",headers:{"Content-Type":"application/json","X-Echo-Key":this.config.writeKey},body:JSON.stringify({userId:e,anonymousId:this.identity.getAnonymousId(),traits:t||{}}),keepalive:true,credentials:"omit"}).catch(()=>{});}setUserProperties(e){let t=this.identity.getUserId();t?this.identify(t,e):this.enqueueEvent("$set_user_properties","auto",e);}page(e){this.enqueueEvent("$pageview","auto",e);}reset(){this.identity.reset(),this.queue.drain();}async flush(){if(this.flushing||this.queue.size()===0)return;this.flushing=true;let e=this.queue.drain();await I(e,{writeKey:this.config.writeKey,endpoint:this.config.endpoint})?this.debugLog("flush ok",e.length):(this.queue.requeue(e),this.debugLog("flush failed, requeued",e.length)),this.flushing=false;}getAnonymousId(){return this.identity.getAnonymousId()}getUserId(){return this.identity.getUserId()}getSessionId(){return this.identity.getSessionId()}startFlushTimer(){typeof setInterval>"u"||(this.flushTimer=setInterval(()=>{this.flush();},this.config.flushInterval));}installPageHideFlush(){if(typeof document>"u")return;let e=()=>{let t=this.queue.drain();t.length!==0&&I(t,{writeKey:this.config.writeKey,endpoint:this.config.endpoint,useBeacon:true});};document.addEventListener("pagehide",e),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&e();});}};var j=["input","textarea","select","[data-private]"],V=["[data-no-track]","script","style"];function T(i,e){for(let t of e)try{if(i.matches(t))return !0}catch{}return false}function l(i,e){let t=e.blockSelectors&&e.blockSelectors.length?e.blockSelectors:V;return T(i,t)}function Y(i,e){let t=e.maskTextSelectors&&e.maskTextSelectors.length?e.maskTextSelectors:j;return T(i,t)}function J(i,e){return Y(i,e)?"":(i.innerText||i.textContent||"").replace(/\s+/g," ").trim().slice(0,200)}function S(i){let e=[],t=i,n=0;for(;t&&t.nodeType===1&&n<6;){let r=t.tagName.toLowerCase();if(t.id){e.unshift(`${r}#${t.id}`);break}let o=(t.getAttribute("class")||"").split(/\s+/).filter(Boolean).slice(0,2).map(u=>"."+u).join(""),s="";if(t.parentElement){let u=Array.from(t.parentElement.children).filter(c=>c.tagName===t.tagName);u.length>1&&(s=`:nth-of-type(${u.indexOf(t)+1})`);}e.unshift(`${r}${o}${s}`),t=t.parentElement,n++;}return e.join(" > ")}function E(i,e){let t=i.tagName.toLowerCase(),n={selector:S(i),tag:t,text:J(i,e)},r=i.id;r&&(n.id=r);let o=i.getAttribute&&i.getAttribute("name");if(o&&(n.name=o),t==="a"){let u=i.href;u&&(n.href=u);}let s=i.getAttribute&&i.getAttribute("aria-label");return s&&(n.ariaLabel=s),n}function v(i){let e=i.tagName?.toLowerCase();if(!e)return false;if(e==="a"||e==="button")return true;let t=i.getAttribute&&i.getAttribute("role");return t==="button"||t==="link"||t==="tab"||t==="menuitem"}function _(i,e,t){typeof document>"u"||document.addEventListener("click",n=>{let r=n.target;if(r){if(e==="instrumented"){let o=r;for(;o&&o!==document.body&&!v(o);)o=o.parentElement;if(!o||o===document.body)return;r=o;}l(r,t)||i.enqueueEvent("$click","auto",E(r,t));}},true);}function k(i){if(typeof window>"u")return;let e=[],t=n=>{let r=Date.now();for(;e.length&&r-e[0].at>5e3;)e.shift();return e.some(o=>o.key===n)?false:(e.push({key:n,at:r}),true)};window.addEventListener("error",n=>{let r=n.message||String(n.error||"unknown"),o=`${r}|${n.filename}|${n.lineno}`;t(o)&&i.enqueueEvent("$error","auto",{message:r,source:n.filename,line:n.lineno,column:n.colno,stack:n.error&&n.error.stack||void 0});}),window.addEventListener("unhandledrejection",n=>{let r=n.reason,o=r instanceof Error?r.message:String(r),s=r instanceof Error?r.stack:void 0,u=`unhandled:${o}`;t(u)&&i.enqueueEvent("$error","auto",{kind:"unhandledrejection",message:o,stack:s});});}function A(i,e){typeof document>"u"||document.addEventListener("submit",t=>{let n=t.target;if(!n||n.tagName!=="FORM"||l(n,e))return;let r=n.querySelectorAll("input, select, textarea").length;i.enqueueEvent("$form_submit","auto",{selector:S(n),id:n.id||void 0,name:n.getAttribute("name")||void 0,action:n.getAttribute("action")||void 0,method:(n.getAttribute("method")||"get").toUpperCase(),fieldCount:r});},true);}var X=500;function x(i,e){if(typeof document>"u")return;let t=null,n=0;document.addEventListener("mouseover",r=>{let o=r.target;if(!o)return;let s=o;for(;s&&s!==document.body&&!v(s);)s=s.parentElement;if(!s||s===document.body||s===t||l(s,e))return;let u=Date.now();if(u-n<X){t=s;return}t=s,n=u,i.enqueueEvent("$hover","auto",E(s,e));},true);}function R(i){if(typeof window>"u"||typeof history>"u")return;let e=location.href,t=0,n=()=>{let o=Date.now();o-t<100||location.href===e&&t!==0||(e=location.href,t=o,i.markPageview(location.href),i.enqueueEvent("$pageview","auto",{url:location.href,path:location.pathname,title:document?.title,referrer:i.getPreviousUrl()}));};n();let r=o=>{let s=history[o];history[o]=function(...u){let c=s.apply(history,u);return n(),c};};r("pushState"),r("replaceState"),window.addEventListener("popstate",n),window.addEventListener("hashchange",n);}function U(i){if(typeof window>"u"||typeof document>"u")return;let e=0,t=0,n=location.href,r=()=>{location.href!==n&&(n=location.href,e=0,t=0);let o=Date.now();if(o-t<250)return;let s=document.documentElement,u=window.scrollY||s.scrollTop||0,c=window.innerHeight||s.clientHeight,C=s.scrollHeight-c;if(C<=0)return;let b=Math.max(0,Math.min(100,Math.round(u/C*100)));b-e<10||(e=b,t=o,i.enqueueEvent("$scroll","auto",{percent:b,pixels:u}));};window.addEventListener("scroll",r,{passive:true});}function L(i){if(typeof document>"u")return;let e=document.visibilityState;document.addEventListener("visibilitychange",()=>{let t=document.visibilityState;t!==e&&(e=t,i.enqueueEvent("$visibility","auto",{state:t}));});}function M(i){if(typeof PerformanceObserver>"u")return;let e=(t,n,r)=>{i.enqueueEvent("$web_vital","auto",{name:t,value:n,...r});};try{new PerformanceObserver(n=>{let r=n.getEntries(),o=r[r.length-1];o&&e("LCP",Math.round(o.startTime));}).observe({type:"largest-contentful-paint",buffered:!0});}catch{}try{let t=0;new PerformanceObserver(r=>{for(let o of r.getEntries())o.hadRecentInput||(t+=o.value);}).observe({type:"layout-shift",buffered:!0}),addEventListener("pagehide",()=>e("CLS",Math.round(t*1e3)/1e3));}catch{}try{new PerformanceObserver(n=>{let r=n.getEntries()[0];r&&e("FID",Math.round(r.processingStart-r.startTime));}).observe({type:"first-input",buffered:!0});}catch{}}var q={pageviews:true,clicks:"all",forms:true,scroll:true,visibility:true,mouseover:true,errors:true,vitals:true,sessions:true};function Q(i){return i===false?null:i===void 0||i===true?q:{...q,...i}}function $(i,e){let t=Q(e.autoCapture);if(!t)return;let n={maskTextSelectors:e.maskTextSelectors,blockSelectors:e.blockSelectors};if(t.pageviews&&R(i),t.clicks){let r=t.clicks==="instrumented"?"instrumented":"all";_(i,r,n);}t.forms&&A(i,n),t.scroll&&U(i),t.visibility&&L(i),t.mouseover&&x(i,n),t.errors&&k(i),t.vitals&&M(i);}function xe(i){let e=new y(i);return $(e,i),e}
2
2
  export{xe as createEcho};
package/package.json CHANGED
@@ -1,21 +1,25 @@
1
1
  {
2
2
  "name": "@mosovn/echo",
3
- "version": "0.0.0",
4
- "description": "Echo web SDK — event tracking with aggressive autocapture for moso products.",
3
+ "version": "0.2.1",
4
+ "description": "Echo SDK — event tracking for moso products. Works in browser and React Native (auto-picked by bundler).",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/moso-vn/echo"
8
+ "url": "https://github.com/LoanFactory-Inc/mosovn-echo",
9
+ "directory": "packages/sdk-web"
9
10
  },
10
11
  "type": "module",
11
- "main": "./dist/index.cjs",
12
- "module": "./dist/index.js",
13
- "types": "./dist/index.d.ts",
12
+ "main": "./dist/web.cjs",
13
+ "module": "./dist/web.js",
14
+ "types": "./dist/web.d.ts",
15
+ "react-native": "./dist/native.js",
14
16
  "exports": {
15
17
  ".": {
16
- "types": "./dist/index.d.ts",
17
- "import": "./dist/index.js",
18
- "require": "./dist/index.cjs"
18
+ "types": "./dist/web.d.ts",
19
+ "react-native": "./dist/native.js",
20
+ "browser": "./dist/web.js",
21
+ "import": "./dist/web.js",
22
+ "require": "./dist/web.cjs"
19
23
  }
20
24
  },
21
25
  "files": [
@@ -29,7 +33,27 @@
29
33
  "lint": "tsc --noEmit",
30
34
  "test": "echo \"no tests yet\""
31
35
  },
36
+ "peerDependencies": {
37
+ "@react-native-async-storage/async-storage": ">=1.19.0",
38
+ "react-native": ">=0.72.0",
39
+ "react-native-get-random-values": ">=1.10.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "@react-native-async-storage/async-storage": {
43
+ "optional": true
44
+ },
45
+ "react-native": {
46
+ "optional": true
47
+ },
48
+ "react-native-get-random-values": {
49
+ "optional": true
50
+ }
51
+ },
32
52
  "devDependencies": {
53
+ "@react-native-async-storage/async-storage": "^1.23.1",
54
+ "@types/react-native": "^0.73.0",
55
+ "react-native": "^0.74.0",
56
+ "react-native-get-random-values": "^1.11.0",
33
57
  "tsup": "^8.3.0",
34
58
  "typescript": "^5.6.0"
35
59
  },
@@ -42,6 +66,7 @@
42
66
  "events",
43
67
  "autocapture",
44
68
  "moso",
69
+ "react-native",
45
70
  "amplitude-alternative"
46
71
  ]
47
72
  }