@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 +79 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +2 -0
- package/package.json +47 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|