@grainql/analytics-web 1.7.1 → 1.7.2

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,4 +1,4 @@
1
- /* Grain Analytics Web SDK v1.7.1 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v1.7.2 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -1,3 +1,3 @@
1
- /* Grain Analytics Web SDK v1.7.1 | MIT License */
1
+ /* Grain Analytics Web SDK v1.7.2 | MIT License */
2
2
  "use strict";var Grain=(()=>{var m=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var v=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var R=(g,t)=>{for(var e in t)m(g,e,{get:t[e],enumerable:!0})},P=(g,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of v(t))!C.call(g,n)&&n!==e&&m(g,n,{get:()=>t[n],enumerable:!(r=E(t,n))||r.enumerable});return g};var w=g=>P(m({},"__esModule",{value:!0}),g);var b={};R(b,{GrainAnalytics:()=>h,createGrainAnalytics:()=>y,default:()=>I});var h=class{constructor(t){this.eventQueue=[];this.flushTimer=null;this.isDestroyed=!1;this.globalUserId=null;this.persistentAnonymousUserId=null;this.configCache=null;this.configRefreshTimer=null;this.configChangeListeners=[];this.configFetchPromise=null;this.config={apiUrl:"https://api.grainql.com",authStrategy:"NONE",batchSize:50,flushInterval:5e3,retryAttempts:3,retryDelay:1e3,maxEventsPerRequest:160,debug:!1,defaultConfigurations:{},configCacheKey:"grain_config",configRefreshInterval:3e5,enableConfigCache:!0,...t,tenantId:t.tenantId},t.userId&&(this.globalUserId=t.userId),this.validateConfig(),this.initializePersistentAnonymousUserId(),this.setupBeforeUnload(),this.startFlushTimer(),this.initializeConfigCache()}validateConfig(){if(!this.config.tenantId)throw new Error("Grain Analytics: tenantId is required");if(this.config.authStrategy==="SERVER_SIDE"&&!this.config.secretKey)throw new Error("Grain Analytics: secretKey is required for SERVER_SIDE auth strategy");if(this.config.authStrategy==="JWT"&&!this.config.authProvider)throw new Error("Grain Analytics: authProvider is required for JWT auth strategy")}generateUUID(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(t){let e=Math.random()*16|0;return(t==="x"?e:e&3|8).toString(16)})}formatAnonymousUserId(t){return`temp:${t.replace(/-/g,"")}`}initializePersistentAnonymousUserId(){if(typeof window>"u")return;let t=`grain_anonymous_user_id_${this.config.tenantId}`;try{let e=localStorage.getItem(t);if(e)this.persistentAnonymousUserId=e,this.log("Loaded persistent anonymous user ID:",this.persistentAnonymousUserId);else{let r=this.generateUUID();this.persistentAnonymousUserId=this.formatAnonymousUserId(r),localStorage.setItem(t,this.persistentAnonymousUserId),this.log("Generated new persistent anonymous user ID:",this.persistentAnonymousUserId)}}catch(e){this.log("Failed to initialize persistent anonymous user ID:",e);let r=this.generateUUID();this.persistentAnonymousUserId=this.formatAnonymousUserId(r)}}getEffectiveUserId(){return this.globalUserId||this.persistentAnonymousUserId||"anonymous"}log(...t){this.config.debug&&console.log("[Grain Analytics]",...t)}createErrorDigest(t){let e=[...new Set(t.map(i=>i.eventName))],r=[...new Set(t.map(i=>i.userId))],n=0,s=0;return t.forEach(i=>{let o=i.properties||{};n+=Object.keys(o).length,s+=JSON.stringify(i).length}),{eventCount:t.length,totalProperties:n,totalSize:s,eventNames:e,userIds:r}}formatError(t,e,r){let n=r?this.createErrorDigest(r):{eventCount:0,totalProperties:0,totalSize:0,eventNames:[],userIds:[]},s="UNKNOWN_ERROR",i="An unknown error occurred";if(t instanceof Error)i=t.message,i.includes("fetch failed")||i.includes("network error")?s="NETWORK_ERROR":i.includes("timeout")?s="TIMEOUT_ERROR":i.includes("HTTP 4")?s="CLIENT_ERROR":i.includes("HTTP 5")?s="SERVER_ERROR":i.includes("JSON")?s="PARSE_ERROR":i.includes("auth")||i.includes("unauthorized")?s="AUTH_ERROR":i.includes("rate limit")||i.includes("429")?s="RATE_LIMIT_ERROR":s="GENERAL_ERROR";else if(typeof t=="string")i=t,s="STRING_ERROR";else if(t&&typeof t=="object"&&"status"in t){let o=t.status;s=`HTTP_${o}`,i=`HTTP ${o} error`}return{code:s,message:i,digest:n,timestamp:new Date().toISOString(),context:e,originalError:t}}logError(t){let{code:e,message:r,digest:n,timestamp:s,context:i}=t,o={"\u{1F6A8} Grain Analytics Error":{"Error Code":e,Message:r,Context:i,Timestamp:s,"Event Digest":{Events:n.eventCount,Properties:n.totalProperties,"Size (bytes)":n.totalSize,"Event Names":n.eventNames.length>0?n.eventNames.join(", "):"None","User IDs":n.userIds.length>0?n.userIds.slice(0,3).join(", ")+(n.userIds.length>3?"...":""):"None"}}};console.error("\u{1F6A8} Grain Analytics Error:",o),this.config.debug&&console.error(`[Grain Analytics] ${e}: ${r} (${i}) - Events: ${n.eventCount}, Props: ${n.totalProperties}, Size: ${n.totalSize}B`)}async safeExecute(t,e,r){try{return await t()}catch(n){let s=this.formatError(n,e,r);return this.logError(s),null}}formatEvent(t){return{eventName:t.eventName,userId:t.userId||this.getEffectiveUserId(),properties:t.properties||{}}}async getAuthHeaders(){let t={"Content-Type":"application/json"};switch(this.config.authStrategy){case"NONE":break;case"SERVER_SIDE":t.Authorization=`Chase ${this.config.secretKey}`;break;case"JWT":if(this.config.authProvider){let e=await this.config.authProvider.getToken();t.Authorization=`Bearer ${e}`}break}return t}async delay(t){return new Promise(e=>setTimeout(e,t))}isRetriableError(t){if(t instanceof Error){let e=t.message.toLowerCase();if(e.includes("fetch failed")||e==="network error"||e.includes("timeout")||e.includes("connection"))return!0}if(typeof t=="object"&&t!==null&&"status"in t){let e=t.status;return e>=500||e===429}return!1}async sendEvents(t){if(t.length===0)return;let e;for(let r=0;r<=this.config.retryAttempts;r++)try{let n=await this.getAuthHeaders(),s=`${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;this.log(`Sending ${t.length} events to ${s} (attempt ${r+1})`);let i=await fetch(s,{method:"POST",headers:n,body:JSON.stringify(t)});if(!i.ok){let o=`HTTP ${i.status}`;try{let c=await i.json();c?.message&&(o=c.message)}catch{let c=await i.text();c&&(o=c)}let a=new Error(`Failed to send events: ${o}`);throw a.status=i.status,a}this.log(`Successfully sent ${t.length} events`);return}catch(n){if(e=n,r===this.config.retryAttempts){let i=this.formatError(n,`sendEvents (attempt ${r+1}/${this.config.retryAttempts+1})`,t);this.logError(i);return}if(!this.isRetriableError(n)){let i=this.formatError(n,"sendEvents (non-retriable error)",t);this.logError(i);return}let s=this.config.retryDelay*Math.pow(2,r);this.log(`Retrying in ${s}ms after error:`,n),await this.delay(s)}}async sendEventsWithBeacon(t){if(t.length!==0)try{let e=await this.getAuthHeaders(),r=`${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`,n=JSON.stringify({events:t});if(typeof navigator<"u"&&"sendBeacon"in navigator){let s=new Blob([n],{type:"application/json"});if(navigator.sendBeacon(r,s)){this.log(`Successfully sent ${t.length} events via beacon`);return}}await fetch(r,{method:"POST",headers:e,body:n,keepalive:!0}),this.log(`Successfully sent ${t.length} events via fetch (keepalive)`)}catch(e){let r=this.formatError(e,"sendEventsWithBeacon",t);this.logError(r)}}startFlushTimer(){this.flushTimer&&clearInterval(this.flushTimer),this.flushTimer=window.setInterval(()=>{this.eventQueue.length>0&&this.flush().catch(t=>{let e=this.formatError(t,"auto-flush");this.logError(e)})},this.config.flushInterval)}setupBeforeUnload(){if(typeof window>"u")return;let t=()=>{if(this.eventQueue.length>0){let e=[...this.eventQueue];this.eventQueue=[];let r=this.chunkEvents(e,this.config.maxEventsPerRequest);r.length>0&&this.sendEventsWithBeacon(r[0]).catch(()=>{})}};window.addEventListener("beforeunload",t),window.addEventListener("pagehide",t),document.addEventListener("visibilitychange",()=>{if(document.visibilityState==="hidden"&&this.eventQueue.length>0){let e=[...this.eventQueue];this.eventQueue=[];let r=this.chunkEvents(e,this.config.maxEventsPerRequest);r.length>0&&this.sendEventsWithBeacon(r[0]).catch(()=>{})}})}async track(t,e,r){try{if(this.isDestroyed){let o=new Error("Grain Analytics: Client has been destroyed"),a=this.formatError(o,"track (client destroyed)");this.logError(a);return}let n,s={};typeof t=="string"?(n={eventName:t,properties:e},s=r||{}):(n=t,s=e||{});let i=this.formatEvent(n);this.eventQueue.push(i),this.log(`Queued event: ${n.eventName}`,n.properties),(s.flush||this.eventQueue.length>=this.config.batchSize)&&await this.flush()}catch(n){let s=this.formatError(n,"track");this.logError(s)}}identify(t){this.log(`Identified user: ${t}`),this.globalUserId=t,this.persistentAnonymousUserId=null}setUserId(t){this.log(`Set global user ID: ${t}`),this.globalUserId=t,t&&(this.persistentAnonymousUserId=null)}getUserId(){return this.globalUserId}getEffectiveUserIdPublic(){return this.getEffectiveUserId()}async setProperty(t,e){try{if(this.isDestroyed){let o=new Error("Grain Analytics: Client has been destroyed"),a=this.formatError(o,"setProperty (client destroyed)");this.logError(a);return}let r=e?.userId||this.getEffectiveUserId(),n=Object.keys(t);if(n.length>4){let o=new Error("Grain Analytics: Maximum 4 properties allowed per request"),a=this.formatError(o,"setProperty (validation)");this.logError(a);return}if(n.length===0){let o=new Error("Grain Analytics: At least one property is required"),a=this.formatError(o,"setProperty (validation)");this.logError(a);return}let s={};for(let[o,a]of Object.entries(t))a==null?s[o]="":typeof a=="string"?s[o]=a:s[o]=JSON.stringify(a);let i={userId:r,...s};await this.sendProperties(i)}catch(r){let n=this.formatError(r,"setProperty");this.logError(n)}}async sendProperties(t){let e;for(let r=0;r<=this.config.retryAttempts;r++)try{let n=await this.getAuthHeaders(),s=`${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/properties`;this.log(`Setting properties for user ${t.userId} (attempt ${r+1})`);let i=await fetch(s,{method:"POST",headers:n,body:JSON.stringify(t)});if(!i.ok){let o=`HTTP ${i.status}`;try{let c=await i.json();c?.message&&(o=c.message)}catch{let c=await i.text();c&&(o=c)}let a=new Error(`Failed to set properties: ${o}`);throw a.status=i.status,a}this.log(`Successfully set properties for user ${t.userId}`);return}catch(n){if(e=n,r===this.config.retryAttempts){let i=this.formatError(n,`sendProperties (attempt ${r+1}/${this.config.retryAttempts+1})`);this.logError(i);return}if(!this.isRetriableError(n)){let i=this.formatError(n,"sendProperties (non-retriable error)");this.logError(i);return}let s=this.config.retryDelay*Math.pow(2,r);this.log(`Retrying in ${s}ms after error:`,n),await this.delay(s)}}async trackLogin(t,e){try{return await this.track("login",t,e)}catch(r){let n=this.formatError(r,"trackLogin");this.logError(n)}}async trackSignup(t,e){try{return await this.track("signup",t,e)}catch(r){let n=this.formatError(r,"trackSignup");this.logError(n)}}async trackCheckout(t,e){try{return await this.track("checkout",t,e)}catch(r){let n=this.formatError(r,"trackCheckout");this.logError(n)}}async trackPageView(t,e){try{return await this.track("page_view",t,e)}catch(r){let n=this.formatError(r,"trackPageView");this.logError(n)}}async trackPurchase(t,e){try{return await this.track("purchase",t,e)}catch(r){let n=this.formatError(r,"trackPurchase");this.logError(n)}}async trackSearch(t,e){try{return await this.track("search",t,e)}catch(r){let n=this.formatError(r,"trackSearch");this.logError(n)}}async trackAddToCart(t,e){try{return await this.track("add_to_cart",t,e)}catch(r){let n=this.formatError(r,"trackAddToCart");this.logError(n)}}async trackRemoveFromCart(t,e){try{return await this.track("remove_from_cart",t,e)}catch(r){let n=this.formatError(r,"trackRemoveFromCart");this.logError(n)}}async flush(){try{if(this.eventQueue.length===0)return;let t=[...this.eventQueue];this.eventQueue=[];let e=this.chunkEvents(t,this.config.maxEventsPerRequest);for(let r of e)await this.sendEvents(r)}catch(t){let e=this.formatError(t,"flush");this.logError(e)}}initializeConfigCache(){if(!(!this.config.enableConfigCache||typeof window>"u"))try{let t=localStorage.getItem(this.config.configCacheKey);t&&(this.configCache=JSON.parse(t),this.log("Loaded configuration from cache:",this.configCache))}catch(t){this.log("Failed to load configuration cache:",t)}}saveConfigCache(t){if(!(!this.config.enableConfigCache||typeof window>"u"))try{localStorage.setItem(this.config.configCacheKey,JSON.stringify(t)),this.log("Saved configuration to cache:",t)}catch(e){this.log("Failed to save configuration cache:",e)}}getConfig(t){if(this.configCache?.configurations?.[t])return this.configCache.configurations[t];if(this.config.defaultConfigurations?.[t])return this.config.defaultConfigurations[t]}getAllConfigs(){let t={...this.config.defaultConfigurations};return this.configCache?.configurations&&Object.assign(t,this.configCache.configurations),t}async fetchConfig(t={}){try{if(this.isDestroyed){let o=new Error("Grain Analytics: Client has been destroyed"),a=this.formatError(o,"fetchConfig (client destroyed)");return this.logError(a),null}let e=t.userId||this.getEffectiveUserId(),r=t.immediateKeys||[],n=t.properties||{},s={userId:e,immediateKeys:r,properties:n},i;for(let o=0;o<=this.config.retryAttempts;o++)try{let a=await this.getAuthHeaders(),c=`${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;this.log(`Fetching configurations for user ${e} (attempt ${o+1})`);let f=await fetch(c,{method:"POST",headers:a,body:JSON.stringify(s)});if(!f.ok){let l=`HTTP ${f.status}`;try{let u=await f.json();u?.message&&(l=u.message)}catch{let u=await f.text();u&&(l=u)}let p=new Error(`Failed to fetch configurations: ${l}`);throw p.status=f.status,p}let d=await f.json();return d.configurations&&this.updateConfigCache(d,e),this.log(`Successfully fetched configurations for user ${e}:`,d),d}catch(a){if(i=a,o===this.config.retryAttempts){let f=this.formatError(a,`fetchConfig (attempt ${o+1}/${this.config.retryAttempts+1})`);return this.logError(f),null}if(!this.isRetriableError(a)){let f=this.formatError(a,"fetchConfig (non-retriable error)");return this.logError(f),null}let c=this.config.retryDelay*Math.pow(2,o);this.log(`Retrying config fetch in ${c}ms after error:`,a),await this.delay(c)}return null}catch(e){let r=this.formatError(e,"fetchConfig");return this.logError(r),null}}async getConfigAsync(t,e={}){try{if(!e.forceRefresh&&this.configCache?.configurations?.[t])return this.configCache.configurations[t];if(!e.forceRefresh&&this.config.defaultConfigurations?.[t])return this.config.defaultConfigurations[t];let r=await this.fetchConfig(e);return r?r.configurations[t]:this.config.defaultConfigurations?.[t]}catch(r){let n=this.formatError(r,"getConfigAsync");return this.logError(n),this.config.defaultConfigurations?.[t]}}async getAllConfigsAsync(t={}){try{if(!t.forceRefresh&&this.configCache?.configurations)return{...this.config.defaultConfigurations,...this.configCache.configurations};let e=await this.fetchConfig(t);return e?{...this.config.defaultConfigurations,...e.configurations}:{...this.config.defaultConfigurations}}catch(e){let r=this.formatError(e,"getAllConfigsAsync");return this.logError(r),{...this.config.defaultConfigurations}}}updateConfigCache(t,e){let r={configurations:t.configurations,snapshotId:t.snapshotId,timestamp:t.timestamp,userId:e},n=this.configCache?.configurations||{};this.configCache=r,this.saveConfigCache(r),JSON.stringify(n)!==JSON.stringify(t.configurations)&&this.notifyConfigChangeListeners(t.configurations)}addConfigChangeListener(t){this.configChangeListeners.push(t)}removeConfigChangeListener(t){let e=this.configChangeListeners.indexOf(t);e>-1&&this.configChangeListeners.splice(e,1)}notifyConfigChangeListeners(t){this.configChangeListeners.forEach(e=>{try{e(t)}catch(r){console.error("[Grain Analytics] Config change listener error:",r)}})}startConfigRefreshTimer(){this.configRefreshTimer&&clearInterval(this.configRefreshTimer),this.configRefreshTimer=window.setInterval(()=>{!this.isDestroyed&&this.globalUserId&&this.fetchConfig().catch(t=>{let e=this.formatError(t,"auto-config refresh");this.logError(e)})},this.config.configRefreshInterval)}stopConfigRefreshTimer(){this.configRefreshTimer&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null)}async preloadConfig(t=[],e){try{if(!this.globalUserId){this.log("Cannot preload config: no user ID set");return}await this.fetchConfig({immediateKeys:t,properties:e})&&this.startConfigRefreshTimer()}catch(r){let n=this.formatError(r,"preloadConfig");this.logError(n)}}chunkEvents(t,e){let r=[];for(let n=0;n<t.length;n+=e)r.push(t.slice(n,n+e));return r}destroy(){if(this.isDestroyed=!0,this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null),this.stopConfigRefreshTimer(),this.configChangeListeners=[],this.eventQueue.length>0){let t=[...this.eventQueue];this.eventQueue=[];let e=this.chunkEvents(t,this.config.maxEventsPerRequest);if(e.length>0){this.sendEventsWithBeacon(e[0]).catch(()=>{});for(let r=1;r<e.length;r++)this.sendEventsWithBeacon(e[r]).catch(()=>{})}}}};function y(g){return new h(g)}var I=h;typeof window<"u"&&(window.Grain={GrainAnalytics:h,createGrainAnalytics:y});return w(b);})();
3
3
  //# sourceMappingURL=index.global.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grainql/analytics-web",
3
- "version": "1.7.1",
3
+ "version": "1.7.2",
4
4
  "description": "Lightweight TypeScript SDK for sending analytics events and managing remote configurations via Grain's REST API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "repository": {
57
57
  "type": "git",
58
- "url": "https://github.com/grain-analytics/web-sdk.git"
58
+ "url": "https://github.com/GrainQL/analytics-web"
59
59
  },
60
60
  "homepage": "https://grainql.com",
61
61
  "private": false,