@mosovn/echo 0.0.0

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 ADDED
@@ -0,0 +1,79 @@
1
+ # @mosovn/echo
2
+
3
+ Web SDK for the Echo event-tracking system. Built for moso products but
4
+ self-contained — works with any Echo-compatible backend.
5
+
6
+ ```bash
7
+ pnpm add @mosovn/echo
8
+ ```
9
+
10
+ ```ts
11
+ import { createEcho } from '@mosovn/echo';
12
+
13
+ const echo = createEcho({
14
+ writeKey: 'pk_xxxxxxxxxxxx',
15
+ endpoint: 'https://events.moso.vn', // optional, this is the default
16
+ });
17
+
18
+ // Custom events
19
+ echo.track('Sign Up', { plan: 'pro' });
20
+
21
+ // Identify a logged-in user
22
+ 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
+ ```
30
+
31
+ ## Autocapture
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
39
+ - `$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
43
+ - `$web_vital` — LCP, FID, CLS
44
+ - `$session_start` / `$session_end` — 30 min inactivity timeout
45
+
46
+ Configure or disable selectively:
47
+
48
+ ```ts
49
+ createEcho({
50
+ writeKey: 'pk_xxx',
51
+ autoCapture: {
52
+ clicks: 'instrumented', // only a/button/[data-track]
53
+ scroll: false,
54
+ },
55
+ maskTextSelectors: ['input', '[data-private]'],
56
+ blockSelectors: ['[data-no-track]'],
57
+ });
58
+ ```
59
+
60
+ ## PII
61
+
62
+ Form field values are NEVER captured. Text inside `<input>`, `<textarea>`,
63
+ `<select>`, and `[data-private]` is masked from autocaptured events by default.
64
+
65
+ Drop a `data-no-track` attribute on any element to exclude it (and its
66
+ descendants) from all autocapture.
67
+
68
+ ## Identity
69
+
70
+ - **anonymous_id** — stable across sessions, stored in localStorage + a cookie
71
+ on the apex domain so subdomains share it.
72
+ - **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.
76
+
77
+ ## License
78
+
79
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +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}
2
+ exports.createEcho=xe;
@@ -0,0 +1,81 @@
1
+ interface EchoConfig {
2
+ /** Project's public write key (e.g. `pk_xxx`). */
3
+ writeKey: string;
4
+ /** API endpoint base, defaults to `https://events.moso.vn`. */
5
+ endpoint?: string;
6
+ /** Number of events to buffer before forcing a flush. Default 50. */
7
+ flushAt?: number;
8
+ /** Max time (ms) to wait before flushing the buffer. Default 10000. */
9
+ flushInterval?: number;
10
+ /** Optional middleware called on every event before queueing. Return `null` to drop. */
11
+ 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
+ /** Inactivity timeout (ms) to end a session. Default 30 min. */
24
+ sessionTimeoutMs?: number;
25
+ /** If true, log internal diagnostic info to console. */
26
+ debug?: boolean;
27
+ }
28
+ interface AutoCaptureConfig {
29
+ pageviews?: boolean;
30
+ /** 'instrumented' = a/button/[data-track]/[role=button]. 'all' = every click. */
31
+ clicks?: boolean | 'instrumented' | 'all';
32
+ forms?: boolean;
33
+ scroll?: boolean;
34
+ visibility?: boolean;
35
+ mouseover?: boolean;
36
+ errors?: boolean;
37
+ vitals?: boolean;
38
+ sessions?: boolean;
39
+ }
40
+ interface EchoEvent {
41
+ /** UUID v7. Client-generated. Doubles as dedup key on the server. */
42
+ _id: string;
43
+ eventName: string;
44
+ eventType: 'auto' | 'custom';
45
+ anonymousId: string;
46
+ userId?: string | null;
47
+ timestamp: string;
48
+ sessionId?: string;
49
+ properties?: Record<string, unknown>;
50
+ context?: Record<string, unknown>;
51
+ }
52
+ interface EchoClient {
53
+ track(eventName: string, properties?: Record<string, unknown>): void;
54
+ identify(userId: string, traits?: Record<string, unknown>): void;
55
+ setUserProperties(traits: Record<string, unknown>): void;
56
+ page(properties?: Record<string, unknown>): void;
57
+ reset(): void;
58
+ flush(): Promise<void>;
59
+ getAnonymousId(): string;
60
+ getUserId(): string | null;
61
+ getSessionId(): string;
62
+ }
63
+
64
+ /**
65
+ * @mosovn/echo — Echo web SDK.
66
+ *
67
+ * Usage:
68
+ *
69
+ * import { createEcho } from '@mosovn/echo';
70
+ *
71
+ * const echo = createEcho({ writeKey: 'pk_xxx' });
72
+ * echo.track('Sign Up', { plan: 'pro' });
73
+ * 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
+ */
78
+
79
+ declare function createEcho(config: EchoConfig): EchoClient;
80
+
81
+ export { type AutoCaptureConfig, type EchoClient, type EchoConfig, type EchoEvent, createEcho };
@@ -0,0 +1,81 @@
1
+ interface EchoConfig {
2
+ /** Project's public write key (e.g. `pk_xxx`). */
3
+ writeKey: string;
4
+ /** API endpoint base, defaults to `https://events.moso.vn`. */
5
+ endpoint?: string;
6
+ /** Number of events to buffer before forcing a flush. Default 50. */
7
+ flushAt?: number;
8
+ /** Max time (ms) to wait before flushing the buffer. Default 10000. */
9
+ flushInterval?: number;
10
+ /** Optional middleware called on every event before queueing. Return `null` to drop. */
11
+ 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
+ /** Inactivity timeout (ms) to end a session. Default 30 min. */
24
+ sessionTimeoutMs?: number;
25
+ /** If true, log internal diagnostic info to console. */
26
+ debug?: boolean;
27
+ }
28
+ interface AutoCaptureConfig {
29
+ pageviews?: boolean;
30
+ /** 'instrumented' = a/button/[data-track]/[role=button]. 'all' = every click. */
31
+ clicks?: boolean | 'instrumented' | 'all';
32
+ forms?: boolean;
33
+ scroll?: boolean;
34
+ visibility?: boolean;
35
+ mouseover?: boolean;
36
+ errors?: boolean;
37
+ vitals?: boolean;
38
+ sessions?: boolean;
39
+ }
40
+ interface EchoEvent {
41
+ /** UUID v7. Client-generated. Doubles as dedup key on the server. */
42
+ _id: string;
43
+ eventName: string;
44
+ eventType: 'auto' | 'custom';
45
+ anonymousId: string;
46
+ userId?: string | null;
47
+ timestamp: string;
48
+ sessionId?: string;
49
+ properties?: Record<string, unknown>;
50
+ context?: Record<string, unknown>;
51
+ }
52
+ interface EchoClient {
53
+ track(eventName: string, properties?: Record<string, unknown>): void;
54
+ identify(userId: string, traits?: Record<string, unknown>): void;
55
+ setUserProperties(traits: Record<string, unknown>): void;
56
+ page(properties?: Record<string, unknown>): void;
57
+ reset(): void;
58
+ flush(): Promise<void>;
59
+ getAnonymousId(): string;
60
+ getUserId(): string | null;
61
+ getSessionId(): string;
62
+ }
63
+
64
+ /**
65
+ * @mosovn/echo — Echo web SDK.
66
+ *
67
+ * Usage:
68
+ *
69
+ * import { createEcho } from '@mosovn/echo';
70
+ *
71
+ * const echo = createEcho({ writeKey: 'pk_xxx' });
72
+ * echo.track('Sign Up', { plan: 'pro' });
73
+ * 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
+ */
78
+
79
+ declare function createEcho(config: EchoConfig): EchoClient;
80
+
81
+ export { type AutoCaptureConfig, type EchoClient, type EchoConfig, type EchoEvent, createEcho };
package/dist/index.js ADDED
@@ -0,0 +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}
2
+ export{xe as createEcho};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@mosovn/echo",
3
+ "version": "0.0.0",
4
+ "description": "Echo web SDK — event tracking with aggressive autocapture for moso products.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/moso-vn/echo"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "lint": "tsc --noEmit",
30
+ "test": "echo \"no tests yet\""
31
+ },
32
+ "devDependencies": {
33
+ "tsup": "^8.3.0",
34
+ "typescript": "^5.6.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=20.0.0"
38
+ },
39
+ "keywords": [
40
+ "analytics",
41
+ "tracking",
42
+ "events",
43
+ "autocapture",
44
+ "moso",
45
+ "amplitude-alternative"
46
+ ]
47
+ }