@bluenath/engage 2.0.3 → 2.0.5

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.
@@ -1,15 +1,27 @@
1
- import { EngageConfig, AnalyticsEvent } from '../types';
1
+ import { EngageConfig, AnalyticsEvent, SentimentValue } from '../types';
2
2
 
3
3
  export declare class EngagePro {
4
4
  private config;
5
5
  private context;
6
6
  private queue;
7
7
  private transport;
8
+ private sessionId;
9
+ private countryCode;
10
+ private currentSentiment;
8
11
  constructor(config: EngageConfig);
9
12
  identify: (userId: string, traits?: Record<string, unknown>) => void;
10
13
  page: (name?: string, properties?: Record<string, unknown>) => void;
11
14
  track: (eventName: string, properties?: Record<string, unknown>, intelligence?: Partial<AnalyticsEvent>) => void;
12
15
  trackRevenue: (amount: number | string, properties?: Record<string, unknown>, intelligence?: Partial<AnalyticsEvent>) => void;
16
+ /**
17
+ * Spec §3.3: Set sentiment for the current session.
18
+ * Attached to the next emitted event.
19
+ */
20
+ setSentiment: (sentiment: SentimentValue) => void;
21
+ /**
22
+ * Emit a new-format EngageEvent to the ingest pipeline (Spec §3.1).
23
+ */
24
+ private emitEngageEvent;
13
25
  private parseRef;
14
26
  private processEvent;
15
27
  }
@@ -1,18 +1,43 @@
1
- import { AnalyticsEvent } from '../types';
1
+ import { AnalyticsEvent, EngageEvent } from '../types';
2
2
  import { Transport } from './Transport';
3
3
 
4
4
  export declare class EventQueue {
5
5
  private queue;
6
+ private engageQueue;
6
7
  private storage;
7
8
  private transport;
8
9
  private readonly STORAGE_KEY;
10
+ private readonly ENGAGE_STORAGE_KEY;
9
11
  private isFlushing;
10
12
  private flushInterval;
11
13
  private retryCount;
14
+ private readonly MAX_RETRIES;
12
15
  constructor(transport: Transport);
13
16
  private startTimer;
17
+ /**
18
+ * Enqueue a legacy AnalyticsEvent (backward compatibility)
19
+ */
14
20
  enqueue(event: AnalyticsEvent): void;
21
+ /**
22
+ * Enqueue a new EngageEvent (Spec §3.1)
23
+ */
24
+ enqueueEngageEvent(event: EngageEvent): void;
15
25
  private save;
26
+ private saveEngage;
16
27
  private load;
28
+ /**
29
+ * Flush legacy events (existing behavior)
30
+ */
17
31
  flush(): Promise<void>;
32
+ /**
33
+ * Spec §3.4: Flush EngageEvent[] with exponential backoff retry.
34
+ * Max 3 retries with 1s/2s/4s backoff. Drop batch after 3 failures.
35
+ * Do not retry on 401/422 (permanent failures).
36
+ */
37
+ flushEngageEvents(): Promise<void>;
38
+ /**
39
+ * Spec §3.4: Use navigator.sendBeacon() to flush remaining events on page unload.
40
+ * This is a best-effort fire-and-forget — no retry possible.
41
+ */
42
+ private flushBeacon;
18
43
  }
@@ -1,7 +1,33 @@
1
- import { AnalyticsEvent } from '../types';
1
+ import { AnalyticsEvent, EngageEvent } from '../types';
2
2
 
3
+ export interface TransportResult {
4
+ success: boolean;
5
+ permanent: boolean;
6
+ status?: number;
7
+ }
3
8
  export declare class Transport {
4
- private apiEndpoint;
5
- constructor(apiEndpoint: string);
9
+ private endpoints;
10
+ private apiKey;
11
+ constructor(endpoints: {
12
+ legacy: string;
13
+ engage: string;
14
+ });
15
+ setApiKey(key: string): void;
16
+ /**
17
+ * Sends legacy batched events (existing format).
18
+ * Used by the old EventQueue for backward compatibility.
19
+ */
6
20
  send(events: AnalyticsEvent[]): Promise<boolean>;
21
+ /**
22
+ * Sends new EngageEvent[] to the ingest endpoint (Spec §4.1).
23
+ * Returns detailed result for retry logic.
24
+ */
25
+ sendEngageEvents(events: EngageEvent[]): Promise<TransportResult>;
26
+ private sendRaw;
27
+ /**
28
+ * Uses sendBeacon for last-resort page unload flush.
29
+ * Cannot send auth headers — the ingest endpoint must also accept
30
+ * the apiKey in the body payload as fallback.
31
+ */
32
+ beaconFlush(payload: string, type: "legacy" | "engage"): boolean;
7
33
  }
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var t=Object.defineProperty,e=(e,n,i)=>((e,n,i)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[n]=i)(e,"symbol"!=typeof n?n+"":n,i);Object.defineProperties(exports,{t:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const n=require("react/jsx-runtime"),i=require("react");let s;const r=new Uint8Array(16);function o(){if(!s&&(s="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto),!s))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return s(r)}const a=[];for(let S=0;S<256;++S)a.push((S+256).toString(16).slice(1));const c={randomUUID:"undefined"!=typeof crypto&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)};function d(t,e,n){if(c.randomUUID&&!t)return c.randomUUID();const i=(t=t||{}).random||(t.rng||o)();return i[6]=15&i[6]|64,i[8]=63&i[8]|128,function(t,e=0){return a[t[e+0]]+a[t[e+1]]+a[t[e+2]]+a[t[e+3]]+"-"+a[t[e+4]]+a[t[e+5]]+"-"+a[t[e+6]]+a[t[e+7]]+"-"+a[t[e+8]]+a[t[e+9]]+"-"+a[t[e+10]]+a[t[e+11]]+a[t[e+12]]+a[t[e+13]]+a[t[e+14]]+a[t[e+15]]}(i)}const h=t=>{let e=2166136261;const n=t.length;for(let i=0;i<n;i++)e^=t.charCodeAt(i),e+=(e<<1)+(e<<4)+(e<<7)+(e<<8)+(e<<24);return("0000000"+(e>>>0).toString(16)).substr(-8)};class u{constructor(t="local"){e(this,"memoryStore",{}),e(this,"keyPrefix","engage_"),e(this,"domain"),this.type=t,this.domain=this.getCookieDomain()}getItem(t){const e=this.keyPrefix+t;if(this.memoryStore.hasOwnProperty(e))return this.memoryStore[e];try{if("local"===this.type&&this.isBrowser())return window.localStorage.getItem(e);if("cookie"===this.type&&this.isBrowser())return this.getCookie(e)}catch(n){return null}return null}setItem(t,e){const n=this.keyPrefix+t;this.memoryStore[n]=e;try{"local"===this.type&&this.isBrowser()?window.localStorage.setItem(n,e):"cookie"===this.type&&this.isBrowser()&&this.setCookie(n,e,365)}catch(i){this.isQuotaError(i)&&(this.type="memory")}}removeItem(t){const e=this.keyPrefix+t;delete this.memoryStore[e];try{"local"===this.type&&this.isBrowser()?window.localStorage.removeItem(e):"cookie"===this.type&&this.isBrowser()&&this.setCookie(e,"",-1)}catch(n){}}getCookie(t){const e=t+"=",n=document.cookie.split(";");for(let i=0;i<n.length;i++){let t=n[i];for(;" "===t.charAt(0);)t=t.substring(1,t.length);if(0===t.indexOf(e))return decodeURIComponent(t.substring(e.length,t.length))}return null}setCookie(t,e,n){let i="";if(n){const t=new Date;t.setTime(t.getTime()+24*n*60*60*1e3),i="; expires="+t.toUTCString()}document.cookie=`${t}=${encodeURIComponent(e)}${i}; path=/; domain=${this.domain}; SameSite=Lax; Secure`}getCookieDomain(){if(!this.isBrowser())return"";const t=window.location.hostname,e=t.split(".");return 1===e.length||"localhost"===t?"":e.length>2?"."+e.slice(-2).join("."):"."+t}isBrowser(){try{return"undefined"!=typeof window&&void 0!==window.document}catch(t){return!1}}isQuotaError(t){return t instanceof DOMException&&(22===t.code||1014===t.code||"QuotaExceededError"===t.name||"NS_ERROR_DOM_QUOTA_REACHED"===t.name)}}class l{constructor(t){e(this,"storage"),e(this,"SESSION_TIMEOUT",18e5),e(this,"deviceId"),e(this,"sessionId"),e(this,"userId",null),e(this,"currentUrl"),e(this,"referrer"),this.storage=new u(t.persistence),this.deviceId=this.getOrSetDeviceId(),this.sessionId="",this.manageSession(),"undefined"!=typeof window?(this.currentUrl=window.location.href,this.referrer=document.referrer,this.listenToHistory()):(this.currentUrl="",this.referrer="")}getOrSetDeviceId(){const t=this.storage.getItem("device_id");if(t)return t;const e=(()=>{if("undefined"==typeof window)return"server-side-id";const t=navigator,e=window.screen,n={userAgent:t.userAgent||"",screenRes:`${e.width}x${e.height}`,colorDepth:e.colorDepth||0,timezone:(new Date).getTimezoneOffset(),language:t.language||"en-US",platform:t.platform||"unknown",hardwareConcurrency:t.hardwareConcurrency||1,deviceMemory:t.deviceMemory||0},i=[n.platform,n.language,n.screenRes,n.colorDepth,n.timezone,n.hardwareConcurrency,n.deviceMemory].join("|");return`${h(i)}-${h(n.userAgent)}`})();return this.storage.setItem("device_id",e),e}manageSession(){const t=Date.now(),e=this.storage.getItem("session_id"),n=parseInt(this.storage.getItem("last_activity")||"0");if(!e||t-n>this.SESSION_TIMEOUT?(this.sessionId=`sess_${t}_${Math.random().toString(36).substr(2,9)}`,this.storage.setItem("session_id",this.sessionId)):this.sessionId=e,this.storage.setItem("last_activity",t.toString()),"undefined"!=typeof window){const t=()=>this.storage.setItem("last_activity",Date.now().toString());window.addEventListener("click",t),window.addEventListener("scroll",t)}}listenToHistory(){const t=history.pushState;history.pushState=(...e)=>{t.apply(history,e),this.handleUrlChange()},window.addEventListener("popstate",()=>this.handleUrlChange())}handleUrlChange(){const t=window.location.href;t!==this.currentUrl&&(this.referrer=this.currentUrl,this.currentUrl=t)}getPayload(){var t;return{library:{name:"@engagepro/analytics",version:"2.0.0"},user:{anonymousId:this.deviceId,id:this.userId},session:{id:this.sessionId,startTime:parseInt(this.sessionId.split("_")[1]||Date.now().toString())},page:{path:"undefined"!=typeof window?window.location.pathname:"",referrer:this.referrer,title:"undefined"!=typeof document?document.title:"",search:"undefined"!=typeof window?window.location.search:"",url:this.currentUrl},network:{online:"undefined"==typeof navigator||navigator.onLine,downlink:null==(t=navigator.connection)?void 0:t.downlink},screen:{width:"undefined"!=typeof screen?screen.width:0,height:"undefined"!=typeof screen?screen.height:0,density:"undefined"!=typeof window?window.devicePixelRatio:1},device:{fingerprint:this.deviceId,type:this.getDeviceType(),userAgent:"undefined"!=typeof navigator?navigator.userAgent:"server"},locale:"undefined"!=typeof navigator?navigator.language:"en-US",timezone:Intl.DateTimeFormat().resolvedOptions().timeZone}}getDeviceType(){if("undefined"==typeof navigator)return"desktop";const t=navigator.userAgent;return/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(t)?"tablet":/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated/.test(t)?"mobile":"desktop"}}class f{constructor(t){e(this,"queue",[]),e(this,"storage"),e(this,"transport"),e(this,"STORAGE_KEY","engage_queue_v1"),e(this,"isFlushing",!1),e(this,"flushInterval"),e(this,"retryCount",0),this.transport=t,this.storage=new u("local"),this.load(),"undefined"!=typeof window&&(window.addEventListener("online",()=>{this.retryCount=0,this.flush()}),window.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&this.flush()})),this.startTimer()}startTimer(){this.flushInterval&&clearInterval(this.flushInterval);const t=3e3*Math.pow(2,this.retryCount);this.flushInterval=setInterval(()=>{this.flush()},Math.min(t,3e4))}enqueue(t){this.queue.push(t),this.save(),(this.queue.length>=10||"PURCHASE"===t.standardEvent)&&this.flush()}save(){this.storage.setItem(this.STORAGE_KEY,JSON.stringify(this.queue))}load(){const t=this.storage.getItem(this.STORAGE_KEY);if(t)try{this.queue=JSON.parse(t)}catch(e){this.queue=[]}}async flush(){if(0===this.queue.length||this.isFlushing)return;if("undefined"!=typeof navigator&&!navigator.onLine)return;this.isFlushing=!0;const t=[...this.queue];this.queue=[],this.save();try{if(!(await this.transport.send(t)))throw new Error("Server rejected batch");this.retryCount=0,this.startTimer()}catch(e){this.queue=[...t,...this.queue],this.save(),this.retryCount++,this.startTimer()}finally{this.isFlushing=!1}}}class p{constructor(t){this.apiEndpoint=t}async send(t){const e=JSON.stringify({batch:t,sentAt:(new Date).toISOString()});if("undefined"!=typeof navigator&&navigator.sendBeacon&&e.length<6e4){const t=new Blob([e],{type:"application/json"});if(navigator.sendBeacon(this.apiEndpoint,t))return!0}try{return(await fetch(this.apiEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:e,keepalive:!0})).ok}catch(n){return!1}}}const w="https://engage-api.bluenath.com/api/v1/tracking/ingest",y=new Set(["localhost","127.0.0.1"]),v={$:"USD",US$:"USD",USD:"USD","₹":"INR",RS:"INR","RS.":"INR",INR:"INR","€":"EUR",EUR:"EUR","£":"GBP",GBP:"GBP","¥":"JPY",JPY:"JPY"},m=t=>{if("string"!=typeof t)return;const e=t.trim().toUpperCase();return e?v[e]?v[e]:e.includes("₹")||e.startsWith("RS")?"INR":e.includes("$")&&!e.includes("CAD")?"USD":e.includes("€")?"EUR":e.includes("£")?"GBP":e.includes("¥")?"JPY":/^[A-Z]{3}$/.test(e)?e:void 0:void 0};class g{constructor(t){e(this,"config"),e(this,"context"),e(this,"queue"),e(this,"transport"),e(this,"identify",(t,e)=>{this.context.userId=t,this.processEvent({event:"Identify",properties:{traits:e},standardEvent:"LOGIN",intent:"identity",confidence:1})}),e(this,"page",(t,e)=>{const n=this.context.getPayload().page;this.processEvent({event:t||n.title||"Unknown Page",properties:{path:n.path,referrer:n.referrer,title:n.title,...e},standardEvent:"PAGE_VIEW",intent:"navigation",confidence:1})}),e(this,"track",(t,e,n)=>{this.processEvent({event:t,properties:e||{},...n})}),e(this,"trackRevenue",(t,e,n)=>{const i=Math.max(0,(t=>{if("number"==typeof t)return Number.isFinite(t)?t:0;const e=t.trim().replace(/[^0-9.+-]/g,""),n=Number(e);return Number.isFinite(n)?n:0})(t)),s=m(null==e?void 0:e.currency)||m(null==e?void 0:e.currencyCode)||m(null==e?void 0:e.currencySymbol)||("string"==typeof t?m(t):void 0)||"USD";this.track("PURCHASE",{...e||{},value:i,amount:i,valuePaise:Math.round(100*i),amountPaise:Math.round(100*i),currency:s,currencyCode:s},{standardEvent:"PURCHASE",intent:"commerce",confidence:1,...n})}),this.config=t,this.transport=new p((t=>{if(!t)return w;try{const e=new URL(t);return e.hostname.endsWith("bluenath.com")||y.has(e.hostname)?(e.pathname="/api/v1/tracking/ingest",e.search="",e.hash="",e.toString()):w}catch{return w}})(t.apiHost)),this.context=new l({persistence:t.tracking.useCookies?"cookie":"local"}),this.queue=new f(this.transport)}parseRef(t){if(!t)return{};const e=t.match(/^camp_([^_]+)_cr_([^_]+)$/);return e?{campaignId:e[1],creatorId:e[2]}:{}}processEvent(t){const e=this.context.getPayload(),n=t.properties.ref,i=("string"==typeof n?n:void 0)||new URLSearchParams(e.page.search||"").get("ref")||void 0,s=this.parseRef(i),r=t.properties.value??t.properties.amount??t.properties.valuePaise??t.properties.amountPaise,o=Number(r),a=t.properties.campaignId,c=t.properties.creatorId,h=t.properties.currency,u={event:t.event,properties:t.properties,standardEvent:t.standardEvent,intent:t.intent,confidence:t.confidence,rawLabel:t.rawLabel,timestamp:(new Date).toISOString(),messageId:d(),writeKey:this.config.writeKey,ref:i,campaignId:("string"==typeof a?a:void 0)||s.campaignId,creatorId:("string"==typeof c?c:void 0)||s.creatorId,value:Number.isFinite(o)?Math.round(o):void 0,valuePaise:Number.isFinite(o)?Math.round(100*o):void 0,currency:("string"==typeof h?h:void 0)||"USD",userId:e.user.id||void 0,anonymousId:e.user.anonymousId,context:e};this.config.debug,this.queue.enqueue(u)}}class b{constructor(t){e(this,"analytics"),e(this,"metrics",{}),this.analytics=t,"undefined"!=typeof window&&"PerformanceObserver"in window&&this.observe()}observe(){try{new PerformanceObserver(t=>{for(const e of t.getEntries()){const t=e;t.hadRecentInput||(this.metrics.cls=(this.metrics.cls||0)+t.value)}}).observe({type:"layout-shift",buffered:!0}),new PerformanceObserver(t=>{const e=t.getEntries(),n=e[e.length-1];this.metrics.lcp=n.renderTime||n.loadTime,this.logMetric("LCP",this.metrics.lcp)}).observe({type:"largest-contentful-paint",buffered:!0}),new PerformanceObserver(t=>{const e=t.getEntries()[0];e&&(this.metrics.fid=e.processingStart-e.startTime,this.logMetric("FID",this.metrics.fid))}).observe({type:"first-input",buffered:!0}),window.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&this.metrics.cls&&this.logMetric("CLS",this.metrics.cls)})}catch(t){}}logMetric(t,e){e<0||this.analytics.track(`Core Web Vital: ${t}`,{metric:t,value:Math.round(e)},{standardEvent:"PERFORMANCE",intent:"performance",confidence:1,rawLabel:`${t}: ${Math.round(e)}ms`})}}const E=i.createContext(null);let R=null;exports.EngageProProvider=({children:t,...e})=>{const s=i.useRef(null),r=i.useRef(null);s.current||(s.current=new g(e),"undefined"!=typeof window&&new b(s.current));const o=s.current;return i.useEffect(()=>{if(!e.tracking.autoTrack)return;const t=t=>{const e=t.target.closest('button, a, input[type="submit"], [data-track], .clickable');if(e){const t=Date.now(),n=r.current;n&&n.el===e&&t-n.ts<500?(n.count++,n.ts=t,3===n.count&&(o.track("Rage Click detected",{element:e.tagName},{standardEvent:"RAGE_CLICK",intent:"frustration",confidence:1}),r.current=null)):r.current={el:e,count:1,ts:t};const i=(t=>{let e=(t.innerText||t.value||t.getAttribute("aria-label")||"").trim();if(!e){const n=t.querySelector("img");n&&n.alt&&(e=n.alt);const i=t.querySelector("title");i&&(e=i.textContent||"")}const n=e.slice(0,100),i=n.toLowerCase(),s=(t.id||"").toLowerCase(),r=t.href||"";return/add to (cart|bag)|buy now/.test(i)||s.includes("add-to-cart")?{standard:"ADD_TO_CART",intent:"commerce",label:n,confidence:.9}:/checkout|proceed/.test(i)||r.includes("/checkout")?{standard:"INITIATE_CHECKOUT",intent:"commerce",label:n,confidence:.9}:/place order|pay now/.test(i)||s.includes("place-order")?{standard:"PURCHASE",intent:"commerce",label:n,confidence:.95}:/cancel order/.test(i)||s.includes("cancel")?{standard:"ORDER_CANCEL",intent:"lifecycle",label:n,confidence:.85}:/refund|return/.test(i)||s.includes("refund")?{standard:"ORDER_REFUND",intent:"lifecycle",label:n,confidence:.85}:/track package|shipping/.test(i)?{standard:"TRACK_PACKAGE",intent:"lifecycle",label:n,confidence:.8}:/write review/.test(i)||i.includes("star")&&/^[1-5]/.test(i)?{standard:"RATE_PRODUCT",intent:"engagement",label:n,confidence:.8}:i.includes("search")||s.includes("search")?{standard:"SEARCH",intent:"search",label:n,confidence:.7}:{standard:"GENERIC",intent:"interaction",label:n,confidence:.5}})(e),s="A"===e.tagName;o.track("Interaction",{element:e.tagName.toLowerCase(),id:e.id,destination:s?e.href:void 0},{standardEvent:i.standard,intent:i.intent,rawLabel:i.label,confidence:i.confidence})}},n=t=>{const e=t.target;(t=>"password"===t.getAttribute("type")||"hidden"===t.getAttribute("type")||/password|cvc|card|cc-num|ssn|credit|hidden/i.test(t.getAttribute("name")||t.id||""))(e)||"focusin"!==t.type||e.dataset.tracked||(e.dataset.tracked="true",o.track("Form Start",{field:e.name||e.id},{standardEvent:"FORM_START",intent:"identity"}))},i=()=>{const t=new URLSearchParams(window.location.search);t.has("q")&&o.track("Search Query",{query:t.get("q")},{standardEvent:"SEARCH",intent:"search"}),requestAnimationFrame(()=>o.page())},s=history.pushState;return history.pushState=(...t)=>{s.apply(history,t),i()},window.addEventListener("popstate",i),window.addEventListener("click",t,!0),window.addEventListener("focusin",n,!0),i(),()=>{history.pushState=s,window.removeEventListener("popstate",i),window.removeEventListener("click",t,!0),window.removeEventListener("focusin",n,!0)}},[e.tracking.autoTrack]),n.jsx(E.Provider,{value:o,children:t})},exports.default=g,exports.init=t=>(R||(R=new g(t)),R),exports.useAnalytics=()=>{const t=i.useContext(E);if(!t)throw new Error("useAnalytics must be used within EngageProProvider");return t};
1
+ "use strict";var e=Object.defineProperty,i=(i,t,a)=>((i,t,a)=>t in i?e(i,t,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[t]=a)(i,"symbol"!=typeof t?t+"":t,a);Object.defineProperties(exports,{i:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const t=require("react/jsx-runtime"),a=require("react");let n;const r=new Uint8Array(16);function s(){if(!n&&(n="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto),!n))throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");return n(r)}const o=[];for(let I=0;I<256;++I)o.push((I+256).toString(16).slice(1));const c={randomUUID:"undefined"!=typeof crypto&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)};function u(e,i,t){if(c.randomUUID&&!e)return c.randomUUID();const a=(e=e||{}).random||(e.rng||s)();return a[6]=15&a[6]|64,a[8]=63&a[8]|128,function(e,i=0){return o[e[i+0]]+o[e[i+1]]+o[e[i+2]]+o[e[i+3]]+"-"+o[e[i+4]]+o[e[i+5]]+"-"+o[e[i+6]]+o[e[i+7]]+"-"+o[e[i+8]]+o[e[i+9]]+"-"+o[e[i+10]]+o[e[i+11]]+o[e[i+12]]+o[e[i+13]]+o[e[i+14]]+o[e[i+15]]}(a)}const d=e=>{let i=2166136261;const t=e.length;for(let a=0;a<t;a++)i^=e.charCodeAt(a),i+=(i<<1)+(i<<4)+(i<<7)+(i<<8)+(i<<24);return("0000000"+(i>>>0).toString(16)).substr(-8)};class h{constructor(e="local"){i(this,"memoryStore",{}),i(this,"keyPrefix","engage_"),i(this,"domain"),this.type=e,this.domain=this.getCookieDomain()}getItem(e){const i=this.keyPrefix+e;if(this.memoryStore.hasOwnProperty(i))return this.memoryStore[i];try{if("local"===this.type&&this.isBrowser())return window.localStorage.getItem(i);if("cookie"===this.type&&this.isBrowser())return this.getCookie(i)}catch(t){return null}return null}setItem(e,i){const t=this.keyPrefix+e;this.memoryStore[t]=i;try{"local"===this.type&&this.isBrowser()?window.localStorage.setItem(t,i):"cookie"===this.type&&this.isBrowser()&&this.setCookie(t,i,365)}catch(a){this.isQuotaError(a)&&(this.type="memory")}}removeItem(e){const i=this.keyPrefix+e;delete this.memoryStore[i];try{"local"===this.type&&this.isBrowser()?window.localStorage.removeItem(i):"cookie"===this.type&&this.isBrowser()&&this.setCookie(i,"",-1)}catch(t){}}getCookie(e){const i=e+"=",t=document.cookie.split(";");for(let a=0;a<t.length;a++){let e=t[a];for(;" "===e.charAt(0);)e=e.substring(1,e.length);if(0===e.indexOf(i))return decodeURIComponent(e.substring(i.length,e.length))}return null}setCookie(e,i,t){let a="";if(t){const e=new Date;e.setTime(e.getTime()+24*t*60*60*1e3),a="; expires="+e.toUTCString()}document.cookie=`${e}=${encodeURIComponent(i)}${a}; path=/; domain=${this.domain}; SameSite=Lax; Secure`}getCookieDomain(){if(!this.isBrowser())return"";const e=window.location.hostname,i=e.split(".");return 1===i.length||"localhost"===e?"":i.length>2?"."+i.slice(-2).join("."):"."+e}isBrowser(){try{return"undefined"!=typeof window&&void 0!==window.document}catch(e){return!1}}isQuotaError(e){return e instanceof DOMException&&(22===e.code||1014===e.code||"QuotaExceededError"===e.name||"NS_ERROR_DOM_QUOTA_REACHED"===e.name)}}class l{constructor(e){i(this,"storage"),i(this,"SESSION_TIMEOUT",18e5),i(this,"deviceId"),i(this,"sessionId"),i(this,"userId",null),i(this,"currentUrl"),i(this,"referrer"),this.storage=new h(e.persistence),this.deviceId=this.getOrSetDeviceId(),this.sessionId="",this.manageSession(),"undefined"!=typeof window?(this.currentUrl=window.location.href,this.referrer=document.referrer,this.listenToHistory()):(this.currentUrl="",this.referrer="")}getOrSetDeviceId(){const e=this.storage.getItem("device_id");if(e)return e;const i=(()=>{if("undefined"==typeof window)return"server-side-id";const e=navigator,i=window.screen,t={userAgent:e.userAgent||"",screenRes:`${i.width}x${i.height}`,colorDepth:i.colorDepth||0,timezone:(new Date).getTimezoneOffset(),language:e.language||"en-US",platform:e.platform||"unknown",hardwareConcurrency:e.hardwareConcurrency||1,deviceMemory:e.deviceMemory||0},a=[t.platform,t.language,t.screenRes,t.colorDepth,t.timezone,t.hardwareConcurrency,t.deviceMemory].join("|");return`${d(a)}-${d(t.userAgent)}`})();return this.storage.setItem("device_id",i),i}manageSession(){const e=Date.now(),i=this.storage.getItem("session_id"),t=parseInt(this.storage.getItem("last_activity")||"0");if(!i||e-t>this.SESSION_TIMEOUT?(this.sessionId=`sess_${e}_${Math.random().toString(36).substr(2,9)}`,this.storage.setItem("session_id",this.sessionId)):this.sessionId=i,this.storage.setItem("last_activity",e.toString()),"undefined"!=typeof window){const e=()=>this.storage.setItem("last_activity",Date.now().toString());window.addEventListener("click",e),window.addEventListener("scroll",e)}}listenToHistory(){const e=history.pushState;history.pushState=(...i)=>{e.apply(history,i),this.handleUrlChange()},window.addEventListener("popstate",()=>this.handleUrlChange())}handleUrlChange(){const e=window.location.href;e!==this.currentUrl&&(this.referrer=this.currentUrl,this.currentUrl=e)}getPayload(){var e;return{library:{name:"@engagepro/analytics",version:"2.0.0"},user:{anonymousId:this.deviceId,id:this.userId},session:{id:this.sessionId,startTime:parseInt(this.sessionId.split("_")[1]||Date.now().toString())},page:{path:"undefined"!=typeof window?window.location.pathname:"",referrer:this.referrer,title:"undefined"!=typeof document?document.title:"",search:"undefined"!=typeof window?window.location.search:"",url:this.currentUrl},network:{online:"undefined"==typeof navigator||navigator.onLine,downlink:null==(e=navigator.connection)?void 0:e.downlink},screen:{width:"undefined"!=typeof screen?screen.width:0,height:"undefined"!=typeof screen?screen.height:0,density:"undefined"!=typeof window?window.devicePixelRatio:1},device:{fingerprint:this.deviceId,type:this.getDeviceType(),userAgent:"undefined"!=typeof navigator?navigator.userAgent:"server"},locale:"undefined"!=typeof navigator?navigator.language:"en-US",timezone:Intl.DateTimeFormat().resolvedOptions().timeZone}}getDeviceType(){if("undefined"==typeof navigator)return"desktop";const e=navigator.userAgent;return/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(e)?"tablet":/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Kindle|Silk-Accelerated/.test(e)?"mobile":"desktop"}}class A{constructor(e){i(this,"queue",[]),i(this,"engageQueue",[]),i(this,"storage"),i(this,"transport"),i(this,"STORAGE_KEY","engage_queue_v1"),i(this,"ENGAGE_STORAGE_KEY","engage_events_v2"),i(this,"isFlushing",!1),i(this,"flushInterval"),i(this,"retryCount",0),i(this,"MAX_RETRIES",3),this.transport=e,this.storage=new h("local"),this.load(),"undefined"!=typeof window&&(window.addEventListener("online",()=>{this.retryCount=0,this.flush()}),window.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&this.flushBeacon()}),window.addEventListener("beforeunload",()=>{this.flushBeacon()})),this.startTimer()}startTimer(){this.flushInterval&&clearInterval(this.flushInterval),this.flushInterval=setInterval(()=>{this.flush()},5e3)}enqueue(e){this.queue.push(e),this.save(),(this.queue.length>=10||"PURCHASE"===e.standardEvent)&&this.flush()}enqueueEngageEvent(e){this.engageQueue.push(e),this.saveEngage(),this.engageQueue.length>=10&&this.flushEngageEvents()}save(){this.storage.setItem(this.STORAGE_KEY,JSON.stringify(this.queue))}saveEngage(){this.storage.setItem(this.ENGAGE_STORAGE_KEY,JSON.stringify(this.engageQueue))}load(){const e=this.storage.getItem(this.STORAGE_KEY);if(e)try{this.queue=JSON.parse(e)}catch(t){this.queue=[]}const i=this.storage.getItem(this.ENGAGE_STORAGE_KEY);if(i)try{this.engageQueue=JSON.parse(i)}catch(t){this.engageQueue=[]}}async flush(){if(0===this.queue.length||this.isFlushing)return void(this.engageQueue.length>0&&!this.isFlushing&&await this.flushEngageEvents());if("undefined"!=typeof navigator&&!navigator.onLine)return;this.isFlushing=!0;const e=[...this.queue];this.queue=[],this.save();try{if(!(await this.transport.send(e)))throw new Error("Server rejected batch");this.retryCount=0}catch(i){this.queue=[...e,...this.queue],this.save(),this.retryCount++}finally{this.isFlushing=!1}this.engageQueue.length>0&&await this.flushEngageEvents()}async flushEngageEvents(){if(0===this.engageQueue.length||this.isFlushing)return;if("undefined"!=typeof navigator&&!navigator.onLine)return;this.isFlushing=!0;const e=[...this.engageQueue];this.engageQueue=[],this.saveEngage();let i=0,t=!1;for(;i<this.MAX_RETRIES&&!t;){const a=await this.transport.sendEngageEvents(e);if(a.success){t=!0;break}if(a.permanent)return void(this.isFlushing=!1);if(i++,i<this.MAX_RETRIES){const e=1e3*Math.pow(2,i-1);await new Promise(i=>setTimeout(i,e))}}this.isFlushing=!1}flushBeacon(){if(this.queue.length>0){const e=JSON.stringify({batch:this.queue,sentAt:(new Date).toISOString()});this.transport.beaconFlush(e,"legacy")&&(this.queue=[],this.save())}if(this.engageQueue.length>0){const e=JSON.stringify({events:this.engageQueue});this.transport.beaconFlush(e,"engage")&&(this.engageQueue=[],this.saveEngage())}}}class f{constructor(e){i(this,"apiKey",""),this.endpoints=e}setApiKey(e){this.apiKey=e}async send(e){return(await this.sendRaw(JSON.stringify({batch:e,sentAt:(new Date).toISOString()}),this.endpoints.legacy)).success}async sendEngageEvents(e){const i=JSON.stringify({events:e});return this.sendRaw(i,this.endpoints.engage)}async sendRaw(e,i){const t={"Content-Type":"application/json"};if(this.apiKey&&(t.Authorization=`Bearer ${this.apiKey}`),!this.apiKey&&"undefined"!=typeof navigator&&navigator.sendBeacon&&e.length<6e4){const t=new Blob([e],{type:"application/json"});if(navigator.sendBeacon(i,t))return{success:!0,permanent:!1}}try{const a=await fetch(i,{method:"POST",headers:t,body:e,keepalive:!0});return 401===a.status||422===a.status?{success:!1,permanent:!0,status:a.status}:{success:a.ok||202===a.status,permanent:!1,status:a.status}}catch(a){return{success:!1,permanent:!1}}}beaconFlush(e,i){if("undefined"==typeof navigator||!navigator.sendBeacon)return!1;const t="legacy"===i?this.endpoints.legacy:this.endpoints.engage,a=new Blob([e],{type:"application/json"});return navigator.sendBeacon(t,a)}}const p={"Asia/Kolkata":"IN","Asia/Calcutta":"IN","Asia/Mumbai":"IN","Asia/Dhaka":"BD","Asia/Kathmandu":"NP","Asia/Colombo":"LK","Asia/Karachi":"PK","Asia/Kabul":"AF","Asia/Tehran":"IR","Asia/Dubai":"AE","Asia/Muscat":"OM","Asia/Bahrain":"BH","Asia/Qatar":"QA","Asia/Kuwait":"KW","Asia/Riyadh":"SA","Asia/Aden":"YE","Asia/Baghdad":"IQ","Asia/Amman":"JO","Asia/Beirut":"LB","Asia/Damascus":"SY","Asia/Jerusalem":"IL","Asia/Tel_Aviv":"IL","Asia/Nicosia":"CY","Asia/Tokyo":"JP","Asia/Seoul":"KR","Asia/Pyongyang":"KP","Asia/Shanghai":"CN","Asia/Chongqing":"CN","Asia/Harbin":"CN","Asia/Urumqi":"CN","Asia/Hong_Kong":"HK","Asia/Macau":"MO","Asia/Taipei":"TW","Asia/Singapore":"SG","Asia/Kuala_Lumpur":"MY","Asia/Brunei":"BN","Asia/Jakarta":"ID","Asia/Makassar":"ID","Asia/Jayapura":"ID","Asia/Bangkok":"TH","Asia/Ho_Chi_Minh":"VN","Asia/Saigon":"VN","Asia/Phnom_Penh":"KH","Asia/Vientiane":"LA","Asia/Yangon":"MM","Asia/Rangoon":"MM","Asia/Manila":"PH","Asia/Ulaanbaatar":"MN","Asia/Hovd":"MN","Asia/Tbilisi":"GE","Asia/Baku":"AZ","Asia/Yerevan":"AM","Asia/Almaty":"KZ","Asia/Bishkek":"KG","Asia/Tashkent":"UZ","Asia/Ashgabat":"TM","Asia/Dushanbe":"TJ","Asia/Thimphu":"BT","Asia/Dili":"TL","Europe/London":"GB","Europe/Dublin":"IE","Europe/Lisbon":"PT","Europe/Madrid":"ES","Europe/Paris":"FR","Europe/Brussels":"BE","Europe/Amsterdam":"NL","Europe/Luxembourg":"LU","Europe/Berlin":"DE","Europe/Zurich":"CH","Europe/Vienna":"AT","Europe/Rome":"IT","Europe/Monaco":"MC","Europe/Vatican":"VA","Europe/Malta":"MT","Europe/Prague":"CZ","Europe/Budapest":"HU","Europe/Warsaw":"PL","Europe/Bratislava":"SK","Europe/Ljubljana":"SI","Europe/Zagreb":"HR","Europe/Belgrade":"RS","Europe/Sarajevo":"BA","Europe/Podgorica":"ME","Europe/Skopje":"MK","Europe/Tirane":"AL","Europe/Sofia":"BG","Europe/Bucharest":"RO","Europe/Chisinau":"MD","Europe/Athens":"GR","Europe/Istanbul":"TR","Europe/Helsinki":"FI","Europe/Stockholm":"SE","Europe/Oslo":"NO","Europe/Copenhagen":"DK","Europe/Tallinn":"EE","Europe/Riga":"LV","Europe/Vilnius":"LT","Europe/Minsk":"BY","Europe/Moscow":"RU","Europe/Kaliningrad":"RU","Europe/Samara":"RU","Europe/Kiev":"UA","Europe/Kyiv":"UA","Europe/Reykjavik":"IS","Europe/Andorra":"AD","Europe/Gibraltar":"GI","Europe/San_Marino":"SM","America/New_York":"US","America/Chicago":"US","America/Denver":"US","America/Los_Angeles":"US","America/Phoenix":"US","America/Anchorage":"US","Pacific/Honolulu":"US","America/Detroit":"US","America/Indianapolis":"US","America/Boise":"US","America/Juneau":"US","America/Adak":"US","America/Toronto":"CA","America/Vancouver":"CA","America/Montreal":"CA","America/Winnipeg":"CA","America/Edmonton":"CA","America/Halifax":"CA","America/St_Johns":"CA","America/Regina":"CA","America/Mexico_City":"MX","America/Cancun":"MX","America/Tijuana":"MX","America/Monterrey":"MX","America/Hermosillo":"MX","America/Guatemala":"GT","America/Belize":"BZ","America/El_Salvador":"SV","America/Tegucigalpa":"HN","America/Managua":"NI","America/Costa_Rica":"CR","America/Panama":"PA","America/Bogota":"CO","America/Lima":"PE","America/Guayaquil":"EC","America/Caracas":"VE","America/La_Paz":"BO","America/Asuncion":"PY","America/Montevideo":"UY","America/Buenos_Aires":"AR","America/Argentina/Buenos_Aires":"AR","America/Santiago":"CL","America/Sao_Paulo":"BR","America/Recife":"BR","America/Manaus":"BR","America/Fortaleza":"BR","America/Bahia":"BR","America/Havana":"CU","America/Jamaica":"JM","America/Port-au-Prince":"HT","America/Santo_Domingo":"DO","America/Puerto_Rico":"PR","America/Port_of_Spain":"TT","America/Barbados":"BB","America/Martinique":"MQ","America/Guyana":"GY","America/Paramaribo":"SR","America/Cayenne":"GF","America/Curacao":"CW","Africa/Cairo":"EG","Africa/Casablanca":"MA","Africa/Tunis":"TN","Africa/Algiers":"DZ","Africa/Tripoli":"LY","Africa/Khartoum":"SD","Africa/Addis_Ababa":"ET","Africa/Nairobi":"KE","Africa/Dar_es_Salaam":"TZ","Africa/Kampala":"UG","Africa/Mogadishu":"SO","Africa/Lagos":"NG","Africa/Accra":"GH","Africa/Abidjan":"CI","Africa/Dakar":"SN","Africa/Bamako":"ML","Africa/Ouagadougou":"BF","Africa/Conakry":"GN","Africa/Freetown":"SL","Africa/Monrovia":"LR","Africa/Lome":"TG","Africa/Porto-Novo":"BJ","Africa/Niamey":"NE","Africa/Douala":"CM","Africa/Libreville":"GA","Africa/Bangui":"CF","Africa/Brazzaville":"CG","Africa/Kinshasa":"CD","Africa/Lubumbashi":"CD","Africa/Luanda":"AO","Africa/Maputo":"MZ","Africa/Harare":"ZW","Africa/Lusaka":"ZM","Africa/Lilongwe":"MW","Africa/Johannesburg":"ZA","Africa/Windhoek":"NA","Africa/Gaborone":"BW","Africa/Maseru":"LS","Africa/Mbabane":"SZ","Indian/Antananarivo":"MG","Indian/Mauritius":"MU","Indian/Reunion":"RE","Indian/Comoro":"KM","Indian/Mayotte":"YT","Africa/Djibouti":"DJ","Africa/Asmara":"ER","Australia/Sydney":"AU","Australia/Melbourne":"AU","Australia/Brisbane":"AU","Australia/Perth":"AU","Australia/Adelaide":"AU","Australia/Hobart":"AU","Australia/Darwin":"AU","Australia/Lord_Howe":"AU","Pacific/Auckland":"NZ","Pacific/Chatham":"NZ","Pacific/Fiji":"FJ","Pacific/Tongatapu":"TO","Pacific/Apia":"WS","Pacific/Port_Moresby":"PG","Pacific/Noumea":"NC","Pacific/Guam":"GU","Pacific/Pago_Pago":"AS","Pacific/Tahiti":"PF","Atlantic/Reykjavik":"IS","Atlantic/Azores":"PT","Atlantic/Canary":"ES","Atlantic/Madeira":"PT","Atlantic/Cape_Verde":"CV","Atlantic/Bermuda":"BM","Indian/Maldives":"MV","Indian/Chagos":"IO","Indian/Christmas":"CX","Indian/Cocos":"CC"},m=new Set(["localhost","127.0.0.1"]),g="engage_session_id",y={$:"USD",US$:"USD",USD:"USD","₹":"INR",RS:"INR","RS.":"INR",INR:"INR","€":"EUR",EUR:"EUR","£":"GBP",GBP:"GBP","¥":"JPY",JPY:"JPY"},v=(e,i,t)=>{if(!e)return t;try{const a=new URL(e);return a.hostname.endsWith("bluenath.com")||m.has(a.hostname)?(a.pathname=i,a.search="",a.hash="",a.toString()):t}catch{return t}},w=e=>{if("string"!=typeof e)return;const i=e.trim().toUpperCase();return i?y[i]?y[i]:i.includes("₹")||i.startsWith("RS")?"INR":i.includes("$")&&!i.includes("CAD")?"USD":i.includes("€")?"EUR":i.includes("£")?"GBP":i.includes("¥")?"JPY":/^[A-Z]{3}$/.test(i)?i:void 0:void 0};function E(e){if(!e||"object"!=typeof e)return;const i={},t=Object.keys(e).slice(0,20);for(const a of t){const t=e[a];"boolean"==typeof t||"number"==typeof t?i[a]=t:"string"==typeof t&&(i[a]=t.slice(0,100))}return Object.keys(i).length>0?i:void 0}class S{constructor(e){i(this,"config"),i(this,"context"),i(this,"queue"),i(this,"transport"),i(this,"sessionId"),i(this,"countryCode"),i(this,"currentSentiment"),i(this,"identify",(e,i)=>{this.context.userId=e,this.processEvent({event:"Identify",properties:{traits:i},standardEvent:"LOGIN",intent:"identity",confidence:1})}),i(this,"page",(e,i)=>{const t=this.context.getPayload().page;this.processEvent({event:e||t.title||"Unknown Page",properties:{path:t.path,referrer:t.referrer,title:t.title,...i},standardEvent:"PAGE_VIEW",intent:"navigation",confidence:1}),this.emitEngageEvent("pageview",i)}),i(this,"track",(e,i,t)=>{this.processEvent({event:e,properties:i||{},...t});const a={purchase:"purchase",PURCHASE:"purchase",signup:"signup",SIGNUP:"signup",register:"signup",REGISTER:"signup",add_to_cart:"add_to_cart",ADD_TO_CART:"add_to_cart",product_view:"product_view",VIEW_CONTENT:"product_view",session_start:"session_start",session_end:"session_end"},n=a[e]||a[(null==t?void 0:t.standardEvent)||""]||"custom";this.emitEngageEvent(n,i)}),i(this,"trackRevenue",(e,i,t)=>{const a=Math.max(0,(e=>{if("number"==typeof e)return Number.isFinite(e)?e:0;const i=e.trim().replace(/[^0-9.+-]/g,""),t=Number(i);return Number.isFinite(t)?t:0})(e)),n=w(null==i?void 0:i.currency)||w(null==i?void 0:i.currencyCode)||w(null==i?void 0:i.currencySymbol)||("string"==typeof e?w(e):void 0)||"USD";this.track("PURCHASE",{...i||{},value:a,amount:a,valuePaise:Math.round(100*a),amountPaise:Math.round(100*a),currency:n,currencyCode:n},{standardEvent:"PURCHASE",intent:"commerce",confidence:1,...t})}),i(this,"setSentiment",e=>{this.currentSentiment=e}),this.config=e;const t=e.apiKey||e.writeKey,a=v(e.apiHost,"/api/v1/tracking/ingest","https://engage-api.bluenath.com/api/v1/tracking/ingest"),n=v(e.apiHost,"/api/v1/analytics/ingest","https://engage-api.bluenath.com/api/v1/analytics/ingest");this.transport=new f({legacy:a,engage:n}),this.transport.setApiKey(t),this.context=new l({persistence:e.tracking.useCookies?"cookie":"local"}),this.queue=new A(this.transport),this.sessionId=function(){if("undefined"==typeof sessionStorage)return u();let e=sessionStorage.getItem(g);return e||(e=u(),sessionStorage.setItem(g,e)),e}(),this.countryCode=function(){try{const e=Intl.DateTimeFormat().resolvedOptions().timeZone;if(!e)return;return p[e]}catch{return}}(),e.tracking.autoTrack&&"undefined"!=typeof window&&this.page()}emitEngageEvent(e,i){const t=this.context.getPayload().page,a={type:e,timestamp:(new Date).toISOString(),sessionId:this.sessionId,userId:this.context.getPayload().user.id||void 0,countryCode:this.countryCode,sentiment:this.currentSentiment,referrer:(t.referrer||"").slice(0,500),path:t.path||("undefined"!=typeof window?window.location.pathname:"/"),currency:w(null==i?void 0:i.currency)||w(null==i?void 0:i.currencyCode)||void 0,amount:"number"==typeof(null==i?void 0:i.amountPaise)?i.amountPaise:"number"==typeof(null==i?void 0:i.amount)?Math.round(100*i.amount):void 0,productId:"string"==typeof(null==i?void 0:i.productId)?i.productId:void 0,productName:"string"==typeof(null==i?void 0:i.productName)?i.productName:void 0,productCategory:"string"==typeof(null==i?void 0:i.productCategory)?i.productCategory:void 0,metadata:E(null==i?void 0:i.metadata)};this.queue.enqueueEngageEvent(a),this.currentSentiment&&(this.currentSentiment=void 0)}parseRef(e){if(!e)return{};const i=e.match(/^camp_([^_]+)_cr_([^_]+)$/);return i?{campaignId:i[1],creatorId:i[2]}:{}}processEvent(e){const i=this.context.getPayload(),t=e.properties.ref,a=("string"==typeof t?t:void 0)||new URLSearchParams(i.page.search||"").get("ref")||void 0,n=this.parseRef(a),r=e.properties.value??e.properties.amount??e.properties.valuePaise??e.properties.amountPaise,s=Number(r),o=e.properties.campaignId,c=e.properties.creatorId,d=e.properties.currency,h={event:e.event,properties:e.properties,standardEvent:e.standardEvent,intent:e.intent,confidence:e.confidence,rawLabel:e.rawLabel,timestamp:(new Date).toISOString(),messageId:u(),writeKey:this.config.writeKey||this.config.apiKey||"",ref:a,campaignId:("string"==typeof o?o:void 0)||n.campaignId,creatorId:("string"==typeof c?c:void 0)||n.creatorId,value:Number.isFinite(s)?Math.round(s):void 0,valuePaise:Number.isFinite(s)?Math.round(100*s):void 0,currency:("string"==typeof d?d:void 0)||"USD",userId:i.user.id||void 0,anonymousId:i.user.anonymousId,context:i};this.config.debug,this.queue.enqueue(h)}}class b{constructor(e){i(this,"analytics"),i(this,"metrics",{}),this.analytics=e,"undefined"!=typeof window&&"PerformanceObserver"in window&&this.observe()}observe(){try{new PerformanceObserver(e=>{for(const i of e.getEntries()){const e=i;e.hadRecentInput||(this.metrics.cls=(this.metrics.cls||0)+e.value)}}).observe({type:"layout-shift",buffered:!0}),new PerformanceObserver(e=>{const i=e.getEntries(),t=i[i.length-1];this.metrics.lcp=t.renderTime||t.loadTime,this.logMetric("LCP",this.metrics.lcp)}).observe({type:"largest-contentful-paint",buffered:!0}),new PerformanceObserver(e=>{const i=e.getEntries()[0];i&&(this.metrics.fid=i.processingStart-i.startTime,this.logMetric("FID",this.metrics.fid))}).observe({type:"first-input",buffered:!0}),window.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&this.metrics.cls&&this.logMetric("CLS",this.metrics.cls)})}catch(e){}}logMetric(e,i){i<0||this.analytics.track(`Core Web Vital: ${e}`,{metric:e,value:Math.round(i)},{standardEvent:"PERFORMANCE",intent:"performance",confidence:1,rawLabel:`${e}: ${Math.round(i)}ms`})}}const C=a.createContext(null);let R=null;const M=e=>{if(!R){const i={...e,writeKey:e.writeKey||e.apiKey||"",apiKey:e.apiKey||e.writeKey||""};R=new S(i)}return R},_={init:e=>M({writeKey:e.apiKey,apiKey:e.apiKey,apiHost:e.apiHost,tracking:e.tracking||{useCookies:!0,fingerprint:!0,autoTrack:!0},debug:e.debug}),track(e,i){R&&R.track(e,i)},page(e,i){R&&R.page(e,i)},identify(e,i){R&&R.identify(e,i)},trackRevenue(e,i){R&&R.trackRevenue(e,i)},setSentiment(e){R&&R.setSentiment(e)}};exports.EngageProProvider=({children:e,...i})=>{const n=a.useRef(null),r=a.useRef(null);n.current||(n.current=new S(i),"undefined"!=typeof window&&new b(n.current));const s=n.current;return a.useEffect(()=>{if(!i.tracking.autoTrack)return;const e=e=>{const i=e.target.closest('button, a, input[type="submit"], [data-track], .clickable');if(i){const e=Date.now(),t=r.current;t&&t.el===i&&e-t.ts<500?(t.count++,t.ts=e,3===t.count&&(s.track("Rage Click detected",{element:i.tagName},{standardEvent:"RAGE_CLICK",intent:"frustration",confidence:1}),r.current=null)):r.current={el:i,count:1,ts:e};const a=(e=>{let i=(e.innerText||e.value||e.getAttribute("aria-label")||"").trim();if(!i){const t=e.querySelector("img");t&&t.alt&&(i=t.alt);const a=e.querySelector("title");a&&(i=a.textContent||"")}const t=i.slice(0,100),a=t.toLowerCase(),n=(e.id||"").toLowerCase(),r=e.href||"";return/add to (cart|bag)|buy now/.test(a)||n.includes("add-to-cart")?{standard:"ADD_TO_CART",intent:"commerce",label:t,confidence:.9}:/checkout|proceed/.test(a)||r.includes("/checkout")?{standard:"INITIATE_CHECKOUT",intent:"commerce",label:t,confidence:.9}:/place order|pay now/.test(a)||n.includes("place-order")?{standard:"PURCHASE",intent:"commerce",label:t,confidence:.95}:/cancel order/.test(a)||n.includes("cancel")?{standard:"ORDER_CANCEL",intent:"lifecycle",label:t,confidence:.85}:/refund|return/.test(a)||n.includes("refund")?{standard:"ORDER_REFUND",intent:"lifecycle",label:t,confidence:.85}:/track package|shipping/.test(a)?{standard:"TRACK_PACKAGE",intent:"lifecycle",label:t,confidence:.8}:/write review/.test(a)||a.includes("star")&&/^[1-5]/.test(a)?{standard:"RATE_PRODUCT",intent:"engagement",label:t,confidence:.8}:a.includes("search")||n.includes("search")?{standard:"SEARCH",intent:"search",label:t,confidence:.7}:{standard:"GENERIC",intent:"interaction",label:t,confidence:.5}})(i),n="A"===i.tagName;s.track("Interaction",{element:i.tagName.toLowerCase(),id:i.id,destination:n?i.href:void 0},{standardEvent:a.standard,intent:a.intent,rawLabel:a.label,confidence:a.confidence})}},t=e=>{const i=e.target;(e=>"password"===e.getAttribute("type")||"hidden"===e.getAttribute("type")||/password|cvc|card|cc-num|ssn|credit|hidden/i.test(e.getAttribute("name")||e.id||""))(i)||"focusin"!==e.type||i.dataset.tracked||(i.dataset.tracked="true",s.track("Form Start",{field:i.name||i.id},{standardEvent:"FORM_START",intent:"identity"}))},a=()=>{const e=new URLSearchParams(window.location.search);e.has("q")&&s.track("Search Query",{query:e.get("q")},{standardEvent:"SEARCH",intent:"search"}),requestAnimationFrame(()=>s.page())},n=history.pushState;return history.pushState=(...e)=>{n.apply(history,e),a()},window.addEventListener("popstate",a),window.addEventListener("click",e,!0),window.addEventListener("focusin",t,!0),a(),()=>{history.pushState=n,window.removeEventListener("popstate",a),window.removeEventListener("click",e,!0),window.removeEventListener("focusin",t,!0)}},[i.tracking.autoTrack]),t.jsx(C.Provider,{value:s,children:e})},exports.default=S,exports.engage=_,exports.init=M,exports.useAnalytics=()=>{const e=a.useContext(C);if(!e)throw new Error("useAnalytics must be used within EngageProProvider");return e};
package/dist/index.d.ts CHANGED
@@ -5,3 +5,20 @@ export default EngagePro;
5
5
  export { EngageProProvider, useAnalytics } from './react/EngageProProvider';
6
6
  export * from './types';
7
7
  export declare const init: (config: EngageConfig) => EngagePro;
8
+ /**
9
+ * Spec §3.5 — The `engage` singleton object for non-React apps.
10
+ * Brands integrate like:
11
+ * import { engage } from '@bluenath/engage';
12
+ * engage.init({ apiKey: 'ep_live_xxxxx' });
13
+ * engage.track('purchase', { amount: 49900, currency: 'INR' });
14
+ */
15
+ export declare const engage: {
16
+ init(config: Partial<EngageConfig> & {
17
+ apiKey: string;
18
+ }): EngagePro;
19
+ track(eventName: string, properties?: Record<string, unknown>): void;
20
+ page(name?: string, properties?: Record<string, unknown>): void;
21
+ identify(userId: string, traits?: Record<string, unknown>): void;
22
+ trackRevenue(amount: number | string, properties?: Record<string, unknown>): void;
23
+ setSentiment(sentiment: "positive" | "neutral" | "negative"): void;
24
+ };
package/dist/index.js CHANGED
@@ -299,14 +299,18 @@ class Context {
299
299
  }
300
300
  }
301
301
  class EventQueue {
302
+ // Spec §3.4
302
303
  constructor(transport) {
303
304
  __publicField(this, "queue", []);
305
+ __publicField(this, "engageQueue", []);
304
306
  __publicField(this, "storage");
305
307
  __publicField(this, "transport");
306
308
  __publicField(this, "STORAGE_KEY", "engage_queue_v1");
309
+ __publicField(this, "ENGAGE_STORAGE_KEY", "engage_events_v2");
307
310
  __publicField(this, "isFlushing", false);
308
311
  __publicField(this, "flushInterval");
309
312
  __publicField(this, "retryCount", 0);
313
+ __publicField(this, "MAX_RETRIES", 3);
310
314
  this.transport = transport;
311
315
  this.storage = new StorageEngine("local");
312
316
  this.load();
@@ -316,21 +320,23 @@ class EventQueue {
316
320
  this.flush();
317
321
  });
318
322
  window.addEventListener("visibilitychange", () => {
319
- if (document.visibilityState === "hidden") this.flush();
323
+ if (document.visibilityState === "hidden") this.flushBeacon();
324
+ });
325
+ window.addEventListener("beforeunload", () => {
326
+ this.flushBeacon();
320
327
  });
321
328
  }
322
329
  this.startTimer();
323
330
  }
324
331
  startTimer() {
325
332
  if (this.flushInterval) clearInterval(this.flushInterval);
326
- const delay = 3e3 * Math.pow(2, this.retryCount);
327
- this.flushInterval = setInterval(
328
- () => {
329
- this.flush();
330
- },
331
- Math.min(delay, 3e4)
332
- );
333
+ this.flushInterval = setInterval(() => {
334
+ this.flush();
335
+ }, 5e3);
333
336
  }
337
+ /**
338
+ * Enqueue a legacy AnalyticsEvent (backward compatibility)
339
+ */
334
340
  enqueue(event) {
335
341
  this.queue.push(event);
336
342
  this.save();
@@ -338,9 +344,25 @@ class EventQueue {
338
344
  this.flush();
339
345
  }
340
346
  }
347
+ /**
348
+ * Enqueue a new EngageEvent (Spec §3.1)
349
+ */
350
+ enqueueEngageEvent(event) {
351
+ this.engageQueue.push(event);
352
+ this.saveEngage();
353
+ if (this.engageQueue.length >= 10) {
354
+ this.flushEngageEvents();
355
+ }
356
+ }
341
357
  save() {
342
358
  this.storage.setItem(this.STORAGE_KEY, JSON.stringify(this.queue));
343
359
  }
360
+ saveEngage() {
361
+ this.storage.setItem(
362
+ this.ENGAGE_STORAGE_KEY,
363
+ JSON.stringify(this.engageQueue)
364
+ );
365
+ }
344
366
  load() {
345
367
  const data = this.storage.getItem(this.STORAGE_KEY);
346
368
  if (data) {
@@ -350,9 +372,25 @@ class EventQueue {
350
372
  this.queue = [];
351
373
  }
352
374
  }
375
+ const engageData = this.storage.getItem(this.ENGAGE_STORAGE_KEY);
376
+ if (engageData) {
377
+ try {
378
+ this.engageQueue = JSON.parse(engageData);
379
+ } catch (e) {
380
+ this.engageQueue = [];
381
+ }
382
+ }
353
383
  }
384
+ /**
385
+ * Flush legacy events (existing behavior)
386
+ */
354
387
  async flush() {
355
- if (this.queue.length === 0 || this.isFlushing) return;
388
+ if (this.queue.length === 0 || this.isFlushing) {
389
+ if (this.engageQueue.length > 0 && !this.isFlushing) {
390
+ await this.flushEngageEvents();
391
+ }
392
+ return;
393
+ }
356
394
  if (typeof navigator !== "undefined" && !navigator.onLine) return;
357
395
  this.isFlushing = true;
358
396
  const batch = [...this.queue];
@@ -362,7 +400,6 @@ class EventQueue {
362
400
  const success = await this.transport.send(batch);
363
401
  if (success) {
364
402
  this.retryCount = 0;
365
- this.startTimer();
366
403
  } else {
367
404
  throw new Error("Server rejected batch");
368
405
  }
@@ -370,45 +407,414 @@ class EventQueue {
370
407
  this.queue = [...batch, ...this.queue];
371
408
  this.save();
372
409
  this.retryCount++;
373
- this.startTimer();
374
410
  } finally {
375
411
  this.isFlushing = false;
376
412
  }
413
+ if (this.engageQueue.length > 0) {
414
+ await this.flushEngageEvents();
415
+ }
416
+ }
417
+ /**
418
+ * Spec §3.4: Flush EngageEvent[] with exponential backoff retry.
419
+ * Max 3 retries with 1s/2s/4s backoff. Drop batch after 3 failures.
420
+ * Do not retry on 401/422 (permanent failures).
421
+ */
422
+ async flushEngageEvents() {
423
+ if (this.engageQueue.length === 0 || this.isFlushing) return;
424
+ if (typeof navigator !== "undefined" && !navigator.onLine) return;
425
+ this.isFlushing = true;
426
+ const batch = [...this.engageQueue];
427
+ this.engageQueue = [];
428
+ this.saveEngage();
429
+ let attempt = 0;
430
+ let success = false;
431
+ while (attempt < this.MAX_RETRIES && !success) {
432
+ const result = await this.transport.sendEngageEvents(batch);
433
+ if (result.success) {
434
+ success = true;
435
+ break;
436
+ }
437
+ if (result.permanent) {
438
+ console.warn(
439
+ `[EngagePro] Permanent failure (${result.status}). Dropping ${batch.length} events.`
440
+ );
441
+ this.isFlushing = false;
442
+ return;
443
+ }
444
+ attempt++;
445
+ if (attempt < this.MAX_RETRIES) {
446
+ const delay = Math.pow(2, attempt - 1) * 1e3;
447
+ await new Promise((resolve) => setTimeout(resolve, delay));
448
+ }
449
+ }
450
+ if (!success) {
451
+ console.warn(
452
+ `[EngagePro] Dropped ${batch.length} events after ${this.MAX_RETRIES} failed retries.`
453
+ );
454
+ }
455
+ this.isFlushing = false;
456
+ }
457
+ /**
458
+ * Spec §3.4: Use navigator.sendBeacon() to flush remaining events on page unload.
459
+ * This is a best-effort fire-and-forget — no retry possible.
460
+ */
461
+ flushBeacon() {
462
+ if (this.queue.length > 0) {
463
+ const payload = JSON.stringify({
464
+ batch: this.queue,
465
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
466
+ });
467
+ const sent = this.transport.beaconFlush(payload, "legacy");
468
+ if (sent) {
469
+ this.queue = [];
470
+ this.save();
471
+ }
472
+ }
473
+ if (this.engageQueue.length > 0) {
474
+ const payload = JSON.stringify({ events: this.engageQueue });
475
+ const sent = this.transport.beaconFlush(payload, "engage");
476
+ if (sent) {
477
+ this.engageQueue = [];
478
+ this.saveEngage();
479
+ }
480
+ }
377
481
  }
378
482
  }
379
483
  class Transport {
380
- constructor(apiEndpoint) {
381
- this.apiEndpoint = apiEndpoint;
484
+ constructor(endpoints) {
485
+ __publicField(this, "apiKey", "");
486
+ this.endpoints = endpoints;
382
487
  }
488
+ setApiKey(key) {
489
+ this.apiKey = key;
490
+ }
491
+ /**
492
+ * Sends legacy batched events (existing format).
493
+ * Used by the old EventQueue for backward compatibility.
494
+ */
383
495
  async send(events) {
384
- const payload = JSON.stringify({
385
- batch: events,
386
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
387
- });
388
- if (typeof navigator !== "undefined" && navigator.sendBeacon && payload.length < 6e4) {
496
+ const result = await this.sendRaw(
497
+ JSON.stringify({ batch: events, sentAt: (/* @__PURE__ */ new Date()).toISOString() }),
498
+ this.endpoints.legacy
499
+ );
500
+ return result.success;
501
+ }
502
+ /**
503
+ * Sends new EngageEvent[] to the ingest endpoint (Spec §4.1).
504
+ * Returns detailed result for retry logic.
505
+ */
506
+ async sendEngageEvents(events) {
507
+ const payload = JSON.stringify({ events });
508
+ return this.sendRaw(payload, this.endpoints.engage);
509
+ }
510
+ async sendRaw(payload, endpoint) {
511
+ const headers = {
512
+ "Content-Type": "application/json"
513
+ };
514
+ if (this.apiKey) {
515
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
516
+ }
517
+ if (!this.apiKey && typeof navigator !== "undefined" && navigator.sendBeacon && payload.length < 6e4) {
389
518
  const blob = new Blob([payload], { type: "application/json" });
390
- const success = navigator.sendBeacon(this.apiEndpoint, blob);
391
- if (success) return true;
519
+ const success = navigator.sendBeacon(endpoint, blob);
520
+ if (success) return { success: true, permanent: false };
392
521
  }
393
522
  try {
394
- const response = await fetch(this.apiEndpoint, {
523
+ const response = await fetch(endpoint, {
395
524
  method: "POST",
396
- headers: { "Content-Type": "application/json" },
525
+ headers,
397
526
  body: payload,
398
527
  keepalive: true
399
- // Tries to keep request alive during navigation
400
528
  });
401
- return response.ok;
529
+ if (response.status === 401 || response.status === 422) {
530
+ return { success: false, permanent: true, status: response.status };
531
+ }
532
+ return {
533
+ success: response.ok || response.status === 202,
534
+ permanent: false,
535
+ status: response.status
536
+ };
402
537
  } catch (e) {
403
538
  console.error("[EngagePro] Transport Failed:", e);
404
- return false;
539
+ return { success: false, permanent: false };
405
540
  }
406
541
  }
542
+ /**
543
+ * Uses sendBeacon for last-resort page unload flush.
544
+ * Cannot send auth headers — the ingest endpoint must also accept
545
+ * the apiKey in the body payload as fallback.
546
+ */
547
+ beaconFlush(payload, type) {
548
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
549
+ const endpoint = type === "legacy" ? this.endpoints.legacy : this.endpoints.engage;
550
+ const blob = new Blob([payload], { type: "application/json" });
551
+ return navigator.sendBeacon(endpoint, blob);
552
+ }
553
+ }
554
+ const TIMEZONE_TO_COUNTRY = {
555
+ // Asia
556
+ "Asia/Kolkata": "IN",
557
+ "Asia/Calcutta": "IN",
558
+ "Asia/Mumbai": "IN",
559
+ "Asia/Dhaka": "BD",
560
+ "Asia/Kathmandu": "NP",
561
+ "Asia/Colombo": "LK",
562
+ "Asia/Karachi": "PK",
563
+ "Asia/Kabul": "AF",
564
+ "Asia/Tehran": "IR",
565
+ "Asia/Dubai": "AE",
566
+ "Asia/Muscat": "OM",
567
+ "Asia/Bahrain": "BH",
568
+ "Asia/Qatar": "QA",
569
+ "Asia/Kuwait": "KW",
570
+ "Asia/Riyadh": "SA",
571
+ "Asia/Aden": "YE",
572
+ "Asia/Baghdad": "IQ",
573
+ "Asia/Amman": "JO",
574
+ "Asia/Beirut": "LB",
575
+ "Asia/Damascus": "SY",
576
+ "Asia/Jerusalem": "IL",
577
+ "Asia/Tel_Aviv": "IL",
578
+ "Asia/Nicosia": "CY",
579
+ "Asia/Tokyo": "JP",
580
+ "Asia/Seoul": "KR",
581
+ "Asia/Pyongyang": "KP",
582
+ "Asia/Shanghai": "CN",
583
+ "Asia/Chongqing": "CN",
584
+ "Asia/Harbin": "CN",
585
+ "Asia/Urumqi": "CN",
586
+ "Asia/Hong_Kong": "HK",
587
+ "Asia/Macau": "MO",
588
+ "Asia/Taipei": "TW",
589
+ "Asia/Singapore": "SG",
590
+ "Asia/Kuala_Lumpur": "MY",
591
+ "Asia/Brunei": "BN",
592
+ "Asia/Jakarta": "ID",
593
+ "Asia/Makassar": "ID",
594
+ "Asia/Jayapura": "ID",
595
+ "Asia/Bangkok": "TH",
596
+ "Asia/Ho_Chi_Minh": "VN",
597
+ "Asia/Saigon": "VN",
598
+ "Asia/Phnom_Penh": "KH",
599
+ "Asia/Vientiane": "LA",
600
+ "Asia/Yangon": "MM",
601
+ "Asia/Rangoon": "MM",
602
+ "Asia/Manila": "PH",
603
+ "Asia/Ulaanbaatar": "MN",
604
+ "Asia/Hovd": "MN",
605
+ "Asia/Tbilisi": "GE",
606
+ "Asia/Baku": "AZ",
607
+ "Asia/Yerevan": "AM",
608
+ "Asia/Almaty": "KZ",
609
+ "Asia/Bishkek": "KG",
610
+ "Asia/Tashkent": "UZ",
611
+ "Asia/Ashgabat": "TM",
612
+ "Asia/Dushanbe": "TJ",
613
+ "Asia/Thimphu": "BT",
614
+ "Asia/Dili": "TL",
615
+ // Europe
616
+ "Europe/London": "GB",
617
+ "Europe/Dublin": "IE",
618
+ "Europe/Lisbon": "PT",
619
+ "Europe/Madrid": "ES",
620
+ "Europe/Paris": "FR",
621
+ "Europe/Brussels": "BE",
622
+ "Europe/Amsterdam": "NL",
623
+ "Europe/Luxembourg": "LU",
624
+ "Europe/Berlin": "DE",
625
+ "Europe/Zurich": "CH",
626
+ "Europe/Vienna": "AT",
627
+ "Europe/Rome": "IT",
628
+ "Europe/Monaco": "MC",
629
+ "Europe/Vatican": "VA",
630
+ "Europe/Malta": "MT",
631
+ "Europe/Prague": "CZ",
632
+ "Europe/Budapest": "HU",
633
+ "Europe/Warsaw": "PL",
634
+ "Europe/Bratislava": "SK",
635
+ "Europe/Ljubljana": "SI",
636
+ "Europe/Zagreb": "HR",
637
+ "Europe/Belgrade": "RS",
638
+ "Europe/Sarajevo": "BA",
639
+ "Europe/Podgorica": "ME",
640
+ "Europe/Skopje": "MK",
641
+ "Europe/Tirane": "AL",
642
+ "Europe/Sofia": "BG",
643
+ "Europe/Bucharest": "RO",
644
+ "Europe/Chisinau": "MD",
645
+ "Europe/Athens": "GR",
646
+ "Europe/Istanbul": "TR",
647
+ "Europe/Helsinki": "FI",
648
+ "Europe/Stockholm": "SE",
649
+ "Europe/Oslo": "NO",
650
+ "Europe/Copenhagen": "DK",
651
+ "Europe/Tallinn": "EE",
652
+ "Europe/Riga": "LV",
653
+ "Europe/Vilnius": "LT",
654
+ "Europe/Minsk": "BY",
655
+ "Europe/Moscow": "RU",
656
+ "Europe/Kaliningrad": "RU",
657
+ "Europe/Samara": "RU",
658
+ "Europe/Kiev": "UA",
659
+ "Europe/Kyiv": "UA",
660
+ "Europe/Reykjavik": "IS",
661
+ "Europe/Andorra": "AD",
662
+ "Europe/Gibraltar": "GI",
663
+ "Europe/San_Marino": "SM",
664
+ // Americas
665
+ "America/New_York": "US",
666
+ "America/Chicago": "US",
667
+ "America/Denver": "US",
668
+ "America/Los_Angeles": "US",
669
+ "America/Phoenix": "US",
670
+ "America/Anchorage": "US",
671
+ "Pacific/Honolulu": "US",
672
+ "America/Detroit": "US",
673
+ "America/Indianapolis": "US",
674
+ "America/Boise": "US",
675
+ "America/Juneau": "US",
676
+ "America/Adak": "US",
677
+ "America/Toronto": "CA",
678
+ "America/Vancouver": "CA",
679
+ "America/Montreal": "CA",
680
+ "America/Winnipeg": "CA",
681
+ "America/Edmonton": "CA",
682
+ "America/Halifax": "CA",
683
+ "America/St_Johns": "CA",
684
+ "America/Regina": "CA",
685
+ "America/Mexico_City": "MX",
686
+ "America/Cancun": "MX",
687
+ "America/Tijuana": "MX",
688
+ "America/Monterrey": "MX",
689
+ "America/Hermosillo": "MX",
690
+ "America/Guatemala": "GT",
691
+ "America/Belize": "BZ",
692
+ "America/El_Salvador": "SV",
693
+ "America/Tegucigalpa": "HN",
694
+ "America/Managua": "NI",
695
+ "America/Costa_Rica": "CR",
696
+ "America/Panama": "PA",
697
+ "America/Bogota": "CO",
698
+ "America/Lima": "PE",
699
+ "America/Guayaquil": "EC",
700
+ "America/Caracas": "VE",
701
+ "America/La_Paz": "BO",
702
+ "America/Asuncion": "PY",
703
+ "America/Montevideo": "UY",
704
+ "America/Buenos_Aires": "AR",
705
+ "America/Argentina/Buenos_Aires": "AR",
706
+ "America/Santiago": "CL",
707
+ "America/Sao_Paulo": "BR",
708
+ "America/Recife": "BR",
709
+ "America/Manaus": "BR",
710
+ "America/Fortaleza": "BR",
711
+ "America/Bahia": "BR",
712
+ "America/Havana": "CU",
713
+ "America/Jamaica": "JM",
714
+ "America/Port-au-Prince": "HT",
715
+ "America/Santo_Domingo": "DO",
716
+ "America/Puerto_Rico": "PR",
717
+ "America/Port_of_Spain": "TT",
718
+ "America/Barbados": "BB",
719
+ "America/Martinique": "MQ",
720
+ "America/Guyana": "GY",
721
+ "America/Paramaribo": "SR",
722
+ "America/Cayenne": "GF",
723
+ "America/Curacao": "CW",
724
+ // Africa
725
+ "Africa/Cairo": "EG",
726
+ "Africa/Casablanca": "MA",
727
+ "Africa/Tunis": "TN",
728
+ "Africa/Algiers": "DZ",
729
+ "Africa/Tripoli": "LY",
730
+ "Africa/Khartoum": "SD",
731
+ "Africa/Addis_Ababa": "ET",
732
+ "Africa/Nairobi": "KE",
733
+ "Africa/Dar_es_Salaam": "TZ",
734
+ "Africa/Kampala": "UG",
735
+ "Africa/Mogadishu": "SO",
736
+ "Africa/Lagos": "NG",
737
+ "Africa/Accra": "GH",
738
+ "Africa/Abidjan": "CI",
739
+ "Africa/Dakar": "SN",
740
+ "Africa/Bamako": "ML",
741
+ "Africa/Ouagadougou": "BF",
742
+ "Africa/Conakry": "GN",
743
+ "Africa/Freetown": "SL",
744
+ "Africa/Monrovia": "LR",
745
+ "Africa/Lome": "TG",
746
+ "Africa/Porto-Novo": "BJ",
747
+ "Africa/Niamey": "NE",
748
+ "Africa/Douala": "CM",
749
+ "Africa/Libreville": "GA",
750
+ "Africa/Bangui": "CF",
751
+ "Africa/Brazzaville": "CG",
752
+ "Africa/Kinshasa": "CD",
753
+ "Africa/Lubumbashi": "CD",
754
+ "Africa/Luanda": "AO",
755
+ "Africa/Maputo": "MZ",
756
+ "Africa/Harare": "ZW",
757
+ "Africa/Lusaka": "ZM",
758
+ "Africa/Lilongwe": "MW",
759
+ "Africa/Johannesburg": "ZA",
760
+ "Africa/Windhoek": "NA",
761
+ "Africa/Gaborone": "BW",
762
+ "Africa/Maseru": "LS",
763
+ "Africa/Mbabane": "SZ",
764
+ "Indian/Antananarivo": "MG",
765
+ "Indian/Mauritius": "MU",
766
+ "Indian/Reunion": "RE",
767
+ "Indian/Comoro": "KM",
768
+ "Indian/Mayotte": "YT",
769
+ "Africa/Djibouti": "DJ",
770
+ "Africa/Asmara": "ER",
771
+ // Oceania
772
+ "Australia/Sydney": "AU",
773
+ "Australia/Melbourne": "AU",
774
+ "Australia/Brisbane": "AU",
775
+ "Australia/Perth": "AU",
776
+ "Australia/Adelaide": "AU",
777
+ "Australia/Hobart": "AU",
778
+ "Australia/Darwin": "AU",
779
+ "Australia/Lord_Howe": "AU",
780
+ "Pacific/Auckland": "NZ",
781
+ "Pacific/Chatham": "NZ",
782
+ "Pacific/Fiji": "FJ",
783
+ "Pacific/Tongatapu": "TO",
784
+ "Pacific/Apia": "WS",
785
+ "Pacific/Port_Moresby": "PG",
786
+ "Pacific/Noumea": "NC",
787
+ "Pacific/Guam": "GU",
788
+ "Pacific/Pago_Pago": "AS",
789
+ "Pacific/Tahiti": "PF",
790
+ // Atlantic / Indian Ocean
791
+ "Atlantic/Reykjavik": "IS",
792
+ "Atlantic/Azores": "PT",
793
+ "Atlantic/Canary": "ES",
794
+ "Atlantic/Madeira": "PT",
795
+ "Atlantic/Cape_Verde": "CV",
796
+ "Atlantic/Bermuda": "BM",
797
+ "Indian/Maldives": "MV",
798
+ "Indian/Chagos": "IO",
799
+ "Indian/Christmas": "CX",
800
+ "Indian/Cocos": "CC"
801
+ };
802
+ function detectCountryFromTimezone() {
803
+ try {
804
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
805
+ if (!tz) return void 0;
806
+ return TIMEZONE_TO_COUNTRY[tz];
807
+ } catch {
808
+ return void 0;
809
+ }
407
810
  }
408
811
  const DEFAULT_INGEST_ENDPOINT = "https://engage-api.bluenath.com/api/v1/tracking/ingest";
812
+ const ENGAGE_INGEST_ENDPOINT = "https://engage-api.bluenath.com/api/v1/analytics/ingest";
409
813
  const FIXED_INGEST_PATH = "/api/v1/tracking/ingest";
814
+ const ENGAGE_INGEST_PATH = "/api/v1/analytics/ingest";
410
815
  const ALLOWED_DEV_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1"]);
411
816
  const DEFAULT_CURRENCY = "USD";
817
+ const SESSION_KEY = "engage_session_id";
412
818
  const SYMBOL_TO_CURRENCY = {
413
819
  $: "USD",
414
820
  US$: "USD",
@@ -424,8 +830,8 @@ const SYMBOL_TO_CURRENCY = {
424
830
  "¥": "JPY",
425
831
  JPY: "JPY"
426
832
  };
427
- const resolveIngestEndpoint = (apiHost) => {
428
- if (!apiHost) return DEFAULT_INGEST_ENDPOINT;
833
+ const resolveEndpoint = (apiHost, path, defaultUrl) => {
834
+ if (!apiHost) return defaultUrl;
429
835
  try {
430
836
  const parsed = new URL(apiHost);
431
837
  const isAllowedHost = parsed.hostname.endsWith("bluenath.com") || ALLOWED_DEV_HOSTS.has(parsed.hostname);
@@ -433,14 +839,14 @@ const resolveIngestEndpoint = (apiHost) => {
433
839
  console.warn(
434
840
  `[EngagePro] Ignoring unsupported apiHost "${parsed.hostname}". Falling back to managed endpoint.`
435
841
  );
436
- return DEFAULT_INGEST_ENDPOINT;
842
+ return defaultUrl;
437
843
  }
438
- parsed.pathname = FIXED_INGEST_PATH;
844
+ parsed.pathname = path;
439
845
  parsed.search = "";
440
846
  parsed.hash = "";
441
847
  return parsed.toString();
442
848
  } catch {
443
- return DEFAULT_INGEST_ENDPOINT;
849
+ return defaultUrl;
444
850
  }
445
851
  };
446
852
  const parseCurrencyToken = (value) => {
@@ -462,6 +868,29 @@ const parseRevenueAmount = (value) => {
462
868
  const parsed = Number(numeric);
463
869
  return Number.isFinite(parsed) ? parsed : 0;
464
870
  };
871
+ function getOrCreateSessionId() {
872
+ if (typeof sessionStorage === "undefined") return v4();
873
+ let sessionId = sessionStorage.getItem(SESSION_KEY);
874
+ if (!sessionId) {
875
+ sessionId = v4();
876
+ sessionStorage.setItem(SESSION_KEY, sessionId);
877
+ }
878
+ return sessionId;
879
+ }
880
+ function validateMetadata(meta) {
881
+ if (!meta || typeof meta !== "object") return void 0;
882
+ const result = {};
883
+ const keys = Object.keys(meta).slice(0, 20);
884
+ for (const key of keys) {
885
+ const val = meta[key];
886
+ if (typeof val === "boolean" || typeof val === "number") {
887
+ result[key] = val;
888
+ } else if (typeof val === "string") {
889
+ result[key] = val.slice(0, 100);
890
+ }
891
+ }
892
+ return Object.keys(result).length > 0 ? result : void 0;
893
+ }
465
894
  class EngagePro {
466
895
  constructor(config) {
467
896
  __publicField(this, "config");
@@ -469,6 +898,11 @@ class EngagePro {
469
898
  __publicField(this, "context");
470
899
  __publicField(this, "queue");
471
900
  __publicField(this, "transport");
901
+ // Spec §3.2: Auto-detected fields
902
+ __publicField(this, "sessionId");
903
+ __publicField(this, "countryCode");
904
+ // Spec §3.3: Current sentiment (set by brand, attached to next event)
905
+ __publicField(this, "currentSentiment");
472
906
  // ✅ ARROW FUNCTION (Auto-bound 'this')
473
907
  __publicField(this, "identify", (userId, traits) => {
474
908
  this.context.userId = userId;
@@ -495,6 +929,7 @@ class EngagePro {
495
929
  intent: "navigation",
496
930
  confidence: 1
497
931
  });
932
+ this.emitEngageEvent("pageview", properties);
498
933
  });
499
934
  // ✅ ARROW FUNCTION
500
935
  __publicField(this, "track", (eventName, properties, intelligence) => {
@@ -503,6 +938,22 @@ class EngagePro {
503
938
  properties: properties || {},
504
939
  ...intelligence
505
940
  });
941
+ const typeMap = {
942
+ purchase: "purchase",
943
+ PURCHASE: "purchase",
944
+ signup: "signup",
945
+ SIGNUP: "signup",
946
+ register: "signup",
947
+ REGISTER: "signup",
948
+ add_to_cart: "add_to_cart",
949
+ ADD_TO_CART: "add_to_cart",
950
+ product_view: "product_view",
951
+ VIEW_CONTENT: "product_view",
952
+ session_start: "session_start",
953
+ session_end: "session_end"
954
+ };
955
+ const engageType = typeMap[eventName] || typeMap[(intelligence == null ? void 0 : intelligence.standardEvent) || ""] || "custom";
956
+ this.emitEngageEvent(engageType, properties);
506
957
  });
507
958
  __publicField(this, "trackRevenue", (amount, properties, intelligence) => {
508
959
  const parsedAmount = Math.max(0, parseRevenueAmount(amount));
@@ -526,12 +977,65 @@ class EngagePro {
526
977
  }
527
978
  );
528
979
  });
980
+ /**
981
+ * Spec §3.3: Set sentiment for the current session.
982
+ * Attached to the next emitted event.
983
+ */
984
+ __publicField(this, "setSentiment", (sentiment) => {
985
+ this.currentSentiment = sentiment;
986
+ });
529
987
  this.config = config;
530
- this.transport = new Transport(resolveIngestEndpoint(config.apiHost));
988
+ const apiKey = config.apiKey || config.writeKey;
989
+ const legacyEndpoint = resolveEndpoint(
990
+ config.apiHost,
991
+ FIXED_INGEST_PATH,
992
+ DEFAULT_INGEST_ENDPOINT
993
+ );
994
+ const engageEndpoint = resolveEndpoint(
995
+ config.apiHost,
996
+ ENGAGE_INGEST_PATH,
997
+ ENGAGE_INGEST_ENDPOINT
998
+ );
999
+ this.transport = new Transport({
1000
+ legacy: legacyEndpoint,
1001
+ engage: engageEndpoint
1002
+ });
1003
+ this.transport.setApiKey(apiKey);
531
1004
  this.context = new Context({
532
1005
  persistence: config.tracking.useCookies ? "cookie" : "local"
533
1006
  });
534
1007
  this.queue = new EventQueue(this.transport);
1008
+ this.sessionId = getOrCreateSessionId();
1009
+ this.countryCode = detectCountryFromTimezone();
1010
+ if (config.tracking.autoTrack && typeof window !== "undefined") {
1011
+ this.page();
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Emit a new-format EngageEvent to the ingest pipeline (Spec §3.1).
1016
+ */
1017
+ emitEngageEvent(type, properties) {
1018
+ const pageCtx = this.context.getPayload().page;
1019
+ const event = {
1020
+ type,
1021
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1022
+ sessionId: this.sessionId,
1023
+ userId: this.context.getPayload().user.id || void 0,
1024
+ countryCode: this.countryCode,
1025
+ sentiment: this.currentSentiment,
1026
+ referrer: (pageCtx.referrer || "").slice(0, 500),
1027
+ path: pageCtx.path || (typeof window !== "undefined" ? window.location.pathname : "/"),
1028
+ currency: parseCurrencyToken(properties == null ? void 0 : properties.currency) || parseCurrencyToken(properties == null ? void 0 : properties.currencyCode) || void 0,
1029
+ amount: typeof (properties == null ? void 0 : properties.amountPaise) === "number" ? properties.amountPaise : typeof (properties == null ? void 0 : properties.amount) === "number" ? Math.round(properties.amount * 100) : void 0,
1030
+ productId: typeof (properties == null ? void 0 : properties.productId) === "string" ? properties.productId : void 0,
1031
+ productName: typeof (properties == null ? void 0 : properties.productName) === "string" ? properties.productName : void 0,
1032
+ productCategory: typeof (properties == null ? void 0 : properties.productCategory) === "string" ? properties.productCategory : void 0,
1033
+ metadata: validateMetadata(properties == null ? void 0 : properties.metadata)
1034
+ };
1035
+ this.queue.enqueueEngageEvent(event);
1036
+ if (this.currentSentiment) {
1037
+ this.currentSentiment = void 0;
1038
+ }
535
1039
  }
536
1040
  parseRef(ref) {
537
1041
  if (!ref) return {};
@@ -539,7 +1043,7 @@ class EngagePro {
539
1043
  if (!m) return {};
540
1044
  return { campaignId: m[1], creatorId: m[2] };
541
1045
  }
542
- // Internal Helper
1046
+ // Internal Helper (legacy event processing)
543
1047
  processEvent(data) {
544
1048
  const baseContext = this.context.getPayload();
545
1049
  const refValue = data.properties.ref;
@@ -561,7 +1065,7 @@ class EngagePro {
561
1065
  // Metadata
562
1066
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
563
1067
  messageId: v4(),
564
- writeKey: this.config.writeKey,
1068
+ writeKey: this.config.writeKey || this.config.apiKey || "",
565
1069
  ref,
566
1070
  campaignId: (typeof campaignIdValue === "string" ? campaignIdValue : void 0) || refParts.campaignId,
567
1071
  creatorId: (typeof creatorIdValue === "string" ? creatorIdValue : void 0) || refParts.creatorId,
@@ -823,13 +1327,69 @@ const EngageProProvider = ({
823
1327
  let instance = null;
824
1328
  const init = (config) => {
825
1329
  if (!instance) {
826
- instance = new EngagePro(config);
1330
+ const normalizedConfig = {
1331
+ ...config,
1332
+ writeKey: config.writeKey || config.apiKey || "",
1333
+ apiKey: config.apiKey || config.writeKey || ""
1334
+ };
1335
+ instance = new EngagePro(normalizedConfig);
827
1336
  }
828
1337
  return instance;
829
1338
  };
1339
+ const engage = {
1340
+ init(config) {
1341
+ return init({
1342
+ writeKey: config.apiKey,
1343
+ apiKey: config.apiKey,
1344
+ apiHost: config.apiHost,
1345
+ tracking: config.tracking || {
1346
+ useCookies: true,
1347
+ fingerprint: true,
1348
+ autoTrack: true
1349
+ },
1350
+ debug: config.debug
1351
+ });
1352
+ },
1353
+ track(eventName, properties) {
1354
+ if (!instance) {
1355
+ console.warn("[EngagePro] Not initialized. Call engage.init() first.");
1356
+ return;
1357
+ }
1358
+ instance.track(eventName, properties);
1359
+ },
1360
+ page(name, properties) {
1361
+ if (!instance) {
1362
+ console.warn("[EngagePro] Not initialized. Call engage.init() first.");
1363
+ return;
1364
+ }
1365
+ instance.page(name, properties);
1366
+ },
1367
+ identify(userId, traits) {
1368
+ if (!instance) {
1369
+ console.warn("[EngagePro] Not initialized. Call engage.init() first.");
1370
+ return;
1371
+ }
1372
+ instance.identify(userId, traits);
1373
+ },
1374
+ trackRevenue(amount, properties) {
1375
+ if (!instance) {
1376
+ console.warn("[EngagePro] Not initialized. Call engage.init() first.");
1377
+ return;
1378
+ }
1379
+ instance.trackRevenue(amount, properties);
1380
+ },
1381
+ setSentiment(sentiment) {
1382
+ if (!instance) {
1383
+ console.warn("[EngagePro] Not initialized. Call engage.init() first.");
1384
+ return;
1385
+ }
1386
+ instance.setSentiment(sentiment);
1387
+ }
1388
+ };
830
1389
  export {
831
1390
  EngageProProvider,
832
1391
  EngagePro as default,
1392
+ engage,
833
1393
  init,
834
1394
  useAnalytics
835
1395
  };
@@ -0,0 +1,6 @@
1
+ export declare const TIMEZONE_TO_COUNTRY: Record<string, string>;
2
+ /**
3
+ * Detects country code from browser timezone.
4
+ * Returns ISO 3166-1 alpha-2 code or undefined.
5
+ */
6
+ export declare function detectCountryFromTimezone(): string | undefined;
@@ -64,6 +64,7 @@ export interface AnalyticsEvent {
64
64
  }
65
65
  export interface EngageConfig {
66
66
  writeKey: string;
67
+ apiKey?: string;
67
68
  apiHost?: string;
68
69
  tracking: {
69
70
  useCookies: boolean;
@@ -72,3 +73,25 @@ export interface EngageConfig {
72
73
  };
73
74
  debug?: boolean;
74
75
  }
76
+ export type EngageEventType = "pageview" | "product_view" | "add_to_cart" | "purchase" | "signup" | "session_start" | "session_end" | "custom";
77
+ export type SentimentValue = "positive" | "neutral" | "negative";
78
+ /**
79
+ * Spec §3.1 — The event schema emitted by the engage module
80
+ * to POST /api/v1/analytics/ingest. Backend validates against this.
81
+ */
82
+ export interface EngageEvent {
83
+ type: EngageEventType;
84
+ timestamp: string;
85
+ sessionId: string;
86
+ userId?: string;
87
+ currency?: string;
88
+ amount?: number;
89
+ productId?: string;
90
+ productName?: string;
91
+ productCategory?: string;
92
+ countryCode?: string;
93
+ sentiment?: SentimentValue;
94
+ referrer?: string;
95
+ path: string;
96
+ metadata?: Record<string, string | number | boolean>;
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluenath/engage",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",