@schematichq/schematic-js 1.2.2 → 1.2.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.
- package/README.md +6 -2
- package/dist/schematic.browser.js +2 -2
- package/dist/schematic.cjs.js +164 -22
- package/dist/schematic.d.ts +25 -4
- package/dist/schematic.esm.js +164 -22
- package/package.json +20 -16
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ pnpm add @schematichq/schematic-js
|
|
|
14
14
|
|
|
15
15
|
## Usage
|
|
16
16
|
|
|
17
|
-
You can use Schematic to identify users; after this, your subsequent track events and flag checks will be associated with this user.
|
|
17
|
+
You can use Schematic to identify users; after this, your subsequent track events and flag checks will be associated with this user.
|
|
18
|
+
|
|
19
|
+
A number of these examples use `keys` to identify companies and users. Learn more about keys [here](https://docs.schematichq.com/developer_resources/key_management).
|
|
18
20
|
|
|
19
21
|
```typescript
|
|
20
22
|
import { Schematic } from "@schematichq/schematic-js";
|
|
@@ -42,9 +44,11 @@ schematic.identify({
|
|
|
42
44
|
|
|
43
45
|
// Send a track event to record usage
|
|
44
46
|
schematic.track({ event: "query" });
|
|
47
|
+
// OR, Send a track event with a quantity to record multiple units of usage
|
|
48
|
+
schematic.track({ event: "query", quantity: 10 });
|
|
45
49
|
|
|
46
50
|
// Check a flag
|
|
47
|
-
await schematic.checkFlag("some-flag-key");
|
|
51
|
+
await schematic.checkFlag({ key: "some-flag-key" });
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
By default, `checkFlag` will perform a network request to get the flag value for this user. If you'd like to check all flags at once in order to minimize network requests, you can use `checkFlags`:
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";(()=>{var
|
|
2
|
-
`)===0?h.substr(1,h.length):h}).forEach(function(h){var g=h.split(":"),l=g.shift().trim();if(l){var x=g.join(":").trim();try{a.append(l,x)}catch(U){console.warn("Response "+U.message)}}}),a}H.call(C.prototype);function v(e,a){if(!(this instanceof v))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');if(a||(a={}),this.type="default",this.status=a.status===void 0?200:a.status,this.status<200||this.status>599)throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].");this.ok=this.status>=200&&this.status<300,this.statusText=a.statusText===void 0?"":""+a.statusText,this.headers=new d(a.headers),this.url=a.url||"",this._initBody(e)}H.call(v.prototype),v.prototype.clone=function(){return new v(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new d(this.headers),url:this.url})},v.error=function(){var e=new v(null,{status:200,statusText:""});return e.ok=!1,e.status=0,e.type="error",e};var re=[301,302,303,307,308];v.redirect=function(e,a){if(re.indexOf(a)===-1)throw new RangeError("Invalid status code");return new v(null,{status:a,headers:{location:e}})},n.DOMException=s.DOMException;try{new n.DOMException}catch{n.DOMException=function(a,o){this.message=a,this.name=o;var h=Error(a);this.stack=h.stack},n.DOMException.prototype=Object.create(Error.prototype),n.DOMException.prototype.constructor=n.DOMException}function I(e,a){return new Promise(function(o,h){var g=new C(e,a);if(g.signal&&g.signal.aborted)return h(new n.DOMException("Aborted","AbortError"));var l=new XMLHttpRequest;function x(){l.abort()}l.onload=function(){var y={statusText:l.statusText,headers:te(l.getAllResponseHeaders()||"")};g.url.indexOf("file://")===0&&(l.status<200||l.status>599)?y.status=200:y.status=l.status,y.url="responseURL"in l?l.responseURL:y.headers.get("X-Request-URL");var R="response"in l?l.response:l.responseText;setTimeout(function(){o(new v(R,y))},0)},l.onerror=function(){setTimeout(function(){h(new TypeError("Network request failed"))},0)},l.ontimeout=function(){setTimeout(function(){h(new TypeError("Network request timed out"))},0)},l.onabort=function(){setTimeout(function(){h(new n.DOMException("Aborted","AbortError"))},0)};function U(y){try{return y===""&&s.location.href?s.location.href:y}catch{return y}}if(l.open(g.method,U(g.url),!0),g.credentials==="include"?l.withCredentials=!0:g.credentials==="omit"&&(l.withCredentials=!1),"responseType"in l&&(i.blob?l.responseType="blob":i.arrayBuffer&&(l.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof d||s.Headers&&a.headers instanceof s.Headers)){var W=[];Object.getOwnPropertyNames(a.headers).forEach(function(y){W.push(p(y)),l.setRequestHeader(y,k(a.headers[y]))}),g.headers.forEach(function(y,R){W.indexOf(R)===-1&&l.setRequestHeader(R,y)})}else g.headers.forEach(function(y,R){l.setRequestHeader(R,y)});g.signal&&(g.signal.addEventListener("abort",x),l.onreadystatechange=function(){l.readyState===4&&g.signal.removeEventListener("abort",x)}),l.send(typeof g._bodyInit>"u"?null:g._bodyInit)})}return I.polyfill=!0,s.fetch||(s.fetch=I,s.Headers=d,s.Request=C,s.Response=v),n.Headers=d,n.Request=C,n.Response=v,n.fetch=I,n}({})})(typeof self<"u"?self:K)});var m=[];for(let r=0;r<256;++r)m.push((r+256).toString(16).slice(1));function $(r,t=0){return(m[r[t+0]]+m[r[t+1]]+m[r[t+2]]+m[r[t+3]]+"-"+m[r[t+4]]+m[r[t+5]]+"-"+m[r[t+6]]+m[r[t+7]]+"-"+m[r[t+8]]+m[r[t+9]]+"-"+m[r[t+10]]+m[r[t+11]]+m[r[t+12]]+m[r[t+13]]+m[r[t+14]]+m[r[t+15]]).toLowerCase()}var P,de=new Uint8Array(16);function A(){if(!P){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");P=crypto.getRandomValues.bind(crypto)}return P(de)}var fe=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),L={randomUUID:fe};function he(r,t,n){if(L.randomUUID&&!t&&!r)return L.randomUUID();r=r||{};let s=r.random??r.rng?.()??A();if(s.length<16)throw new Error("Random bytes length must be >= 16");if(s[6]=s[6]&15|64,s[8]=s[8]&63|128,t){if(n=n||0,n<0||n+16>t.length)throw new RangeError(`UUID byte range ${n}:${n+15} is out of buffer bounds`);for(let i=0;i<16;++i)t[n+i]=s[i];return t}return $(s)}var _=he;var rt=ue(z());function w(r){return ge(r,!1)}function ge(r,t){return r==null?r:{companyId:r.company_id==null?void 0:r.company_id,error:r.error==null?void 0:r.error,featureAllocation:r.feature_allocation==null?void 0:r.feature_allocation,featureUsage:r.feature_usage==null?void 0:r.feature_usage,featureUsagePeriod:r.feature_usage_period==null?void 0:r.feature_usage_period,featureUsageResetAt:r.feature_usage_reset_at==null?void 0:new Date(r.feature_usage_reset_at),flag:r.flag,flagId:r.flag_id==null?void 0:r.flag_id,reason:r.reason,ruleId:r.rule_id==null?void 0:r.rule_id,ruleType:r.rule_type==null?void 0:r.rule_type,userId:r.user_id==null?void 0:r.user_id,value:r.value}}function B(r){return ye(r,!1)}function ye(r,t=!1){return r==null?r:{company_id:r.companyId,error:r.error,flag_id:r.flagId,flag_key:r.flagKey,reason:r.reason,req_company:r.reqCompany,req_user:r.reqUser,rule_id:r.ruleId,user_id:r.userId,value:r.value}}function T(r){return be(r,!1)}function be(r,t){return r==null?r:{data:w(r.data),params:r.params}}function X(r){return ke(r,!1)}function ke(r,t){return r==null?r:{flags:r.flags.map(w)}}function N(r){return Fe(r,!1)}function Fe(r,t){return r==null?r:{data:X(r.data),params:r.params}}var O=r=>{let{companyId:t,error:n,featureAllocation:s,featureUsage:i,featureUsagePeriod:c,featureUsageResetAt:u,flag:f,flagId:p,reason:k,ruleId:E,ruleType:d,userId:F,value:b}=w(r);return{featureUsageExceeded:!b&&(d=="company_override_usage_exceeded"||d=="plan_entitlement_usage_exceeded"),companyId:t??void 0,error:n??void 0,featureAllocation:s??void 0,featureUsage:i??void 0,featureUsagePeriod:c??void 0,featureUsageResetAt:u??void 0,flag:f,flagId:p??void 0,reason:k,ruleId:E??void 0,ruleType:d??void 0,userId:F??void 0,value:b}};function S(r){let t=Object.keys(r).reduce((n,s)=>{let c=Object.keys(r[s]||{}).sort().reduce((u,f)=>(u[f]=r[s][f],u),{});return n[s]=c,n},{});return JSON.stringify(t)}var J="1.2.2";var G="schematicId";var D=class{additionalHeaders={};apiKey;apiUrl="https://api.schematichq.com";conn=null;context={};debugEnabled=!1;offlineEnabled=!1;eventQueue;eventUrl="https://c.schematichq.com";flagCheckListeners={};flagValueListeners={};isPending=!0;isPendingListeners=new Set;storage;useWebSocket=!1;checks={};webSocketUrl="wss://api.schematichq.com";constructor(t,n){if(this.apiKey=t,this.eventQueue=[],this.useWebSocket=n?.useWebSocket??!1,this.debugEnabled=n?.debug??!1,this.offlineEnabled=n?.offline??!1,typeof window<"u"&&typeof window.location<"u"){let s=new URLSearchParams(window.location.search),i=s.get("schematic_debug");i!==null&&(i===""||i==="true"||i==="1")&&(this.debugEnabled=!0);let c=s.get("schematic_offline");c!==null&&(c===""||c==="true"||c==="1")&&(this.offlineEnabled=!0,this.debugEnabled=!0)}this.offlineEnabled&&n?.debug!==!1&&(this.debugEnabled=!0),this.offlineEnabled&&this.setIsPending(!1),this.additionalHeaders={"X-Schematic-Client-Version":`schematic-js@${J}`,...n?.additionalHeaders??{}},n?.storage?this.storage=n.storage:typeof localStorage<"u"&&(this.storage=localStorage),n?.apiUrl!==void 0&&(this.apiUrl=n.apiUrl),n?.eventUrl!==void 0&&(this.eventUrl=n.eventUrl),n?.webSocketUrl!==void 0&&(this.webSocketUrl=n.webSocketUrl),typeof window<"u"&&window?.addEventListener&&window.addEventListener("beforeunload",()=>{this.flushEventQueue()}),this.offlineEnabled?this.debug("Initialized with offline mode enabled - no network requests will be made"):this.debugEnabled&&this.debug("Initialized with debug mode enabled")}async checkFlag(t){let{fallback:n=!1,key:s}=t,i=t.context||this.context,c=S(i);if(this.debug(`checkFlag: ${s}`,{context:i,fallback:n}),this.isOffline())return this.debug(`checkFlag offline result: ${s}`,{value:n,offlineMode:!0}),n;if(!this.useWebSocket){let u=`${this.apiUrl}/flags/${s}/check`;return fetch(u,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(i)}).then(f=>{if(!f.ok)throw new Error("Network response was not ok");return f.json()}).then(f=>{let p=T(f);this.debug(`checkFlag result: ${s}`,p);let k=O(p.data);return this.submitFlagCheckEvent(s,k,i),k.value}).catch(f=>{console.error("There was a problem with the fetch operation:",f);let p={flag:s,value:n,reason:"API request failed",error:f instanceof Error?f.message:String(f)};return this.submitFlagCheckEvent(s,p,i),n})}try{let u=this.checks[c];if(this.conn!==null&&typeof u<"u"&&typeof u[s]<"u")return this.debug(`checkFlag cached result: ${s}`,u[s]),u[s].value;if(this.isOffline())return n;try{await this.setContext(i)}catch(E){return console.error("WebSocket connection failed, falling back to REST:",E),this.fallbackToRest(s,i,n)}let p=(this.checks[c]??{})[s],k=p?.value??n;return this.debug(`checkFlag WebSocket result: ${s}`,typeof p<"u"?p:{value:n,fallbackUsed:!0}),typeof p<"u"&&this.submitFlagCheckEvent(s,p,i),k}catch(u){console.error("Unexpected error in checkFlag:",u);let f={flag:s,value:n,reason:"Unexpected error in flag check",error:u instanceof Error?u.message:String(u)};return this.submitFlagCheckEvent(s,f,i),n}}debug(t,...n){this.debugEnabled&&console.log(`[Schematic] ${t}`,...n)}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(t,n,s){let i={flagKey:t,value:n.value,reason:n.reason,flagId:n.flagId,ruleId:n.ruleId,companyId:n.companyId,userId:n.userId,error:n.error,reqCompany:s.company,reqUser:s.user};return this.debug("submitting flag check event:",i),this.handleEvent("flag_check",B(i))}async fallbackToRest(t,n,s){if(this.isOffline())return this.debug(`fallbackToRest offline result: ${t}`,{value:s,offlineMode:!0}),s;try{let i=`${this.apiUrl}/flags/${t}/check`,c=await fetch(i,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(n)});if(!c.ok)throw new Error("Network response was not ok");let u=await c.json(),f=T(u);this.debug(`fallbackToRest result: ${t}`,f);let p=O(f.data);return this.submitFlagCheckEvent(t,p,n),p.value}catch(i){console.error("REST API call failed, using fallback value:",i);let c={flag:t,value:s,reason:"API request failed (fallback)",error:i instanceof Error?i.message:String(i)};return this.submitFlagCheckEvent(t,c,n),s}}checkFlags=async t=>{if(t=t||this.context,this.debug("checkFlags",{context:t}),this.isOffline())return this.debug("checkFlags offline result: returning empty object"),{};let n=`${this.apiUrl}/flags/check`,s=JSON.stringify(t);return fetch(n,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:s}).then(i=>{if(!i.ok)throw new Error("Network response was not ok");return i.json()}).then(i=>{let c=N(i);return this.debug("checkFlags result:",c),(c?.data?.flags??[]).reduce((u,f)=>(u[f.flag]=f.value,u),{})}).catch(i=>(console.error("There was a problem with the fetch operation:",i),{}))};identify=t=>{this.debug("identify:",t);try{this.setContext({company:t.company?.keys,user:t.keys})}catch(n){console.error("Error setting context:",n)}return this.handleEvent("identify",t)};setContext=async t=>{if(this.isOffline())return this.context=t,this.setIsPending(!1),Promise.resolve();if(!this.useWebSocket)return Promise.resolve();try{this.setIsPending(!0),this.conn||(this.conn=this.wsConnect());let n=await this.conn;await this.wsSendMessage(n,t)}catch(n){throw console.error("Failed to establish WebSocket connection:",n),n}};track=t=>{let{company:n,user:s,event:i,traits:c}=t,u={company:n??this.context.company,event:i,traits:c??{},user:s??this.context.user};return this.debug("track:",u),this.handleEvent("track",u)};flushEventQueue=()=>{for(;this.eventQueue.length>0;){let t=this.eventQueue.shift();t&&this.sendEvent(t)}};getAnonymousId=()=>{if(!this.storage)return _();let t=this.storage.getItem(G);if(typeof t<"u")return t;let n=_();return this.storage.setItem(G,n),n};handleEvent=(t,n)=>{let s={api_key:this.apiKey,body:n,sent_at:new Date().toISOString(),tracker_event_id:_(),tracker_user_id:this.getAnonymousId(),type:t};return document?.hidden?this.storeEvent(s):this.sendEvent(s)};sendEvent=async t=>{let n=`${this.eventUrl}/e`,s=JSON.stringify(t);if(this.debug("sending event:",{url:n,event:t}),this.isOffline())return this.debug("event not sent (offline mode):",{event:t}),Promise.resolve();try{let i=await fetch(n,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:s});this.debug("event sent:",{status:i.status,statusText:i.statusText})}catch(i){console.error("Error sending Schematic event: ",i)}return Promise.resolve()};storeEvent=t=>(this.eventQueue.push(t),Promise.resolve());cleanup=async()=>{if(this.isOffline())return this.debug("cleanup: skipped (offline mode)"),Promise.resolve();if(this.conn)try{(await this.conn).close()}catch(t){console.error("Error during cleanup:",t)}finally{this.conn=null}};wsConnect=()=>this.isOffline()?(this.debug("wsConnect: skipped (offline mode)"),Promise.reject(new Error("WebSocket connection skipped in offline mode"))):new Promise((t,n)=>{let s=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",s);let i=new WebSocket(s);i.onopen=()=>{this.debug("WebSocket connection opened"),t(i)},i.onerror=c=>{this.debug("WebSocket connection error:",c),n(c)},i.onclose=()=>{this.debug("WebSocket connection closed"),this.conn=null}});wsSendMessage=(t,n)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((s,i)=>{if(S(n)==S(this.context))return this.debug("WebSocket context unchanged, skipping update"),s(this.setIsPending(!1));this.debug("WebSocket context updated:",n),this.context=n;let c=()=>{let u=!1,f=E=>{let d=JSON.parse(E.data);this.debug("WebSocket message received:",d),S(n)in this.checks||(this.checks[S(n)]={}),(d.flags??[]).forEach(F=>{let b=O(F);this.checks[S(n)][b.flag]=b,this.debug("WebSocket flag update:",{flag:b.flag,value:b.value,flagCheck:b}),(this.flagCheckListeners[F.flag]?.size>0||this.flagValueListeners[F.flag]?.size>0)&&this.submitFlagCheckEvent(b.flag,b,n),this.notifyFlagCheckListeners(F.flag,b),this.notifyFlagValueListeners(F.flag,b.value)}),this.setIsPending(!1),u||(u=!0,s())};t.addEventListener("message",f);let p=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${J}`,k={apiKey:this.apiKey,clientVersion:p,data:n};this.debug("WebSocket sending message:",k),t.send(JSON.stringify(k))};t.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending message"),c()):t.readyState===WebSocket.CONNECTING?(this.debug("WebSocket connecting, waiting for open to send message"),t.addEventListener("open",c)):(this.debug("WebSocket is closed, cannot send message"),i("WebSocket is not open or connecting"))});getIsPending=()=>this.isPending;addIsPendingListener=t=>(this.isPendingListeners.add(t),()=>{this.isPendingListeners.delete(t)});setIsPending=t=>{this.isPending=t,this.isPendingListeners.forEach(n=>ve(n,t))};getFlagCheck=t=>{let n=S(this.context);return(this.checks[n]??{})[t]};getFlagValue=t=>this.getFlagCheck(t)?.value;addFlagValueListener=(t,n)=>(t in this.flagValueListeners||(this.flagValueListeners[t]=new Set),this.flagValueListeners[t].add(n),()=>{this.flagValueListeners[t].delete(n)});addFlagCheckListener=(t,n)=>(t in this.flagCheckListeners||(this.flagCheckListeners[t]=new Set),this.flagCheckListeners[t].add(n),()=>{this.flagCheckListeners[t].delete(n)});notifyFlagCheckListeners=(t,n)=>{let s=this.flagCheckListeners?.[t]??[];s.size>0&&this.debug(`Notifying ${s.size} flag check listeners for ${t}`,n),s.forEach(i=>Ee(i,n))};notifyFlagValueListeners=(t,n)=>{let s=this.flagValueListeners?.[t]??[];s.size>0&&this.debug(`Notifying ${s.size} flag value listeners for ${t}`,{value:n}),s.forEach(i=>Ce(i,n))}},ve=(r,t)=>{r.length>0?r(t):r()},Ee=(r,t)=>{r.length>0?r(t):r()},Ce=(r,t)=>{r.length>0?r(t):r()};window.Schematic=D;})();
|
|
1
|
+
"use strict";(()=>{var re=Object.create;var W=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ae=Object.getOwnPropertyNames;var ie=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var le=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);var ue=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of ae(e))!oe.call(r,a)&&a!==t&&W(r,a,{get:()=>e[a],enumerable:!(s=se(e,a))||s.enumerable});return r};var ce=(r,e,t)=>(t=r!=null?re(ie(r)):{},ue(e||!r||!r.__esModule?W(t,"default",{value:r,enumerable:!0}):t,r));var z=le(Q=>{(function(r){var e=(function(t){var s=typeof globalThis<"u"&&globalThis||typeof r<"u"&&r||typeof global<"u"&&global||{},a={searchParams:"URLSearchParams"in s,iterable:"Symbol"in s&&"iterator"in Symbol,blob:"FileReader"in s&&"Blob"in s&&(function(){try{return new Blob,!0}catch{return!1}})(),formData:"FormData"in s,arrayBuffer:"ArrayBuffer"in s};function c(n){return n&&DataView.prototype.isPrototypeOf(n)}if(a.arrayBuffer)var o=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],u=ArrayBuffer.isView||function(n){return n&&o.indexOf(Object.prototype.toString.call(n))>-1};function f(n){if(typeof n!="string"&&(n=String(n)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(n)||n==="")throw new TypeError('Invalid character in header field name: "'+n+'"');return n.toLowerCase()}function y(n){return typeof n!="string"&&(n=String(n)),n}function F(n){var i={next:function(){var l=n.shift();return{done:l===void 0,value:l}}};return a.iterable&&(i[Symbol.iterator]=function(){return i}),i}function g(n){this.map={},n instanceof g?n.forEach(function(i,l){this.append(l,i)},this):Array.isArray(n)?n.forEach(function(i){if(i.length!=2)throw new TypeError("Headers constructor: expected name/value pair to be length 2, found"+i.length);this.append(i[0],i[1])},this):n&&Object.getOwnPropertyNames(n).forEach(function(i){this.append(i,n[i])},this)}g.prototype.append=function(n,i){n=f(n),i=y(i);var l=this.map[n];this.map[n]=l?l+", "+i:i},g.prototype.delete=function(n){delete this.map[f(n)]},g.prototype.get=function(n){return n=f(n),this.has(n)?this.map[n]:null},g.prototype.has=function(n){return this.map.hasOwnProperty(f(n))},g.prototype.set=function(n,i){this.map[f(n)]=y(i)},g.prototype.forEach=function(n,i){for(var l in this.map)this.map.hasOwnProperty(l)&&n.call(i,this.map[l],l,this)},g.prototype.keys=function(){var n=[];return this.forEach(function(i,l){n.push(l)}),F(n)},g.prototype.values=function(){var n=[];return this.forEach(function(i){n.push(i)}),F(n)},g.prototype.entries=function(){var n=[];return this.forEach(function(i,l){n.push([l,i])}),F(n)},a.iterable&&(g.prototype[Symbol.iterator]=g.prototype.entries);function k(n){if(!n._noBody){if(n.bodyUsed)return Promise.reject(new TypeError("Already read"));n.bodyUsed=!0}}function m(n){return new Promise(function(i,l){n.onload=function(){i(n.result)},n.onerror=function(){l(n.error)}})}function x(n){var i=new FileReader,l=m(i);return i.readAsArrayBuffer(n),l}function V(n){var i=new FileReader,l=m(i),h=/charset=([A-Za-z0-9_-]+)/.exec(n.type),p=h?h[1]:"utf-8";return i.readAsText(n,p),l}function Y(n){for(var i=new Uint8Array(n),l=new Array(i.length),h=0;h<i.length;h++)l[h]=String.fromCharCode(i[h]);return l.join("")}function q(n){if(n.slice)return n.slice(0);var i=new Uint8Array(n.byteLength);return i.set(new Uint8Array(n)),i.buffer}function $(){return this.bodyUsed=!1,this._initBody=function(n){this.bodyUsed=this.bodyUsed,this._bodyInit=n,n?typeof n=="string"?this._bodyText=n:a.blob&&Blob.prototype.isPrototypeOf(n)?this._bodyBlob=n:a.formData&&FormData.prototype.isPrototypeOf(n)?this._bodyFormData=n:a.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)?this._bodyText=n.toString():a.arrayBuffer&&a.blob&&c(n)?(this._bodyArrayBuffer=q(n.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):a.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(n)||u(n))?this._bodyArrayBuffer=q(n):this._bodyText=n=Object.prototype.toString.call(n):(this._noBody=!0,this._bodyText=""),this.headers.get("content-type")||(typeof n=="string"?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):a.searchParams&&URLSearchParams.prototype.isPrototypeOf(n)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},a.blob&&(this.blob=function(){var n=k(this);if(n)return n;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(new Blob([this._bodyArrayBuffer]));if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))}),this.arrayBuffer=function(){if(this._bodyArrayBuffer){var n=k(this);return n||(ArrayBuffer.isView(this._bodyArrayBuffer)?Promise.resolve(this._bodyArrayBuffer.buffer.slice(this._bodyArrayBuffer.byteOffset,this._bodyArrayBuffer.byteOffset+this._bodyArrayBuffer.byteLength)):Promise.resolve(this._bodyArrayBuffer))}else{if(a.blob)return this.blob().then(x);throw new Error("could not read as ArrayBuffer")}},this.text=function(){var n=k(this);if(n)return n;if(this._bodyBlob)return V(this._bodyBlob);if(this._bodyArrayBuffer)return Promise.resolve(Y(this._bodyArrayBuffer));if(this._bodyFormData)throw new Error("could not read FormData body as text");return Promise.resolve(this._bodyText)},a.formData&&(this.formData=function(){return this.text().then(ee)}),this.json=function(){return this.text().then(JSON.parse)},this}var Z=["CONNECT","DELETE","GET","HEAD","OPTIONS","PATCH","POST","PUT","TRACE"];function j(n){var i=n.toUpperCase();return Z.indexOf(i)>-1?i:n}function S(n,i){if(!(this instanceof S))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');i=i||{};var l=i.body;if(n instanceof S){if(n.bodyUsed)throw new TypeError("Already read");this.url=n.url,this.credentials=n.credentials,i.headers||(this.headers=new g(n.headers)),this.method=n.method,this.mode=n.mode,this.signal=n.signal,!l&&n._bodyInit!=null&&(l=n._bodyInit,n.bodyUsed=!0)}else this.url=String(n);if(this.credentials=i.credentials||this.credentials||"same-origin",(i.headers||!this.headers)&&(this.headers=new g(i.headers)),this.method=j(i.method||this.method||"GET"),this.mode=i.mode||this.mode||null,this.signal=i.signal||this.signal||(function(){if("AbortController"in s){var d=new AbortController;return d.signal}})(),this.referrer=null,(this.method==="GET"||this.method==="HEAD")&&l)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(l),(this.method==="GET"||this.method==="HEAD")&&(i.cache==="no-store"||i.cache==="no-cache")){var h=/([?&])_=[^&]*/;if(h.test(this.url))this.url=this.url.replace(h,"$1_="+new Date().getTime());else{var p=/\?/;this.url+=(p.test(this.url)?"&":"?")+"_="+new Date().getTime()}}}S.prototype.clone=function(){return new S(this,{body:this._bodyInit})};function ee(n){var i=new FormData;return n.trim().split("&").forEach(function(l){if(l){var h=l.split("="),p=h.shift().replace(/\+/g," "),d=h.join("=").replace(/\+/g," ");i.append(decodeURIComponent(p),decodeURIComponent(d))}}),i}function te(n){var i=new g,l=n.replace(/\r?\n[\t ]+/g," ");return l.split("\r").map(function(h){return h.indexOf(`
|
|
2
|
+
`)===0?h.substr(1,h.length):h}).forEach(function(h){var p=h.split(":"),d=p.shift().trim();if(d){var U=p.join(":").trim();try{i.append(d,U)}catch(A){console.warn("Response "+A.message)}}}),i}$.call(S.prototype);function E(n,i){if(!(this instanceof E))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');if(i||(i={}),this.type="default",this.status=i.status===void 0?200:i.status,this.status<200||this.status>599)throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].");this.ok=this.status>=200&&this.status<300,this.statusText=i.statusText===void 0?"":""+i.statusText,this.headers=new g(i.headers),this.url=i.url||"",this._initBody(n)}$.call(E.prototype),E.prototype.clone=function(){return new E(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new g(this.headers),url:this.url})},E.error=function(){var n=new E(null,{status:200,statusText:""});return n.ok=!1,n.status=0,n.type="error",n};var ne=[301,302,303,307,308];E.redirect=function(n,i){if(ne.indexOf(i)===-1)throw new RangeError("Invalid status code");return new E(null,{status:i,headers:{location:n}})},t.DOMException=s.DOMException;try{new t.DOMException}catch{t.DOMException=function(i,l){this.message=i,this.name=l;var h=Error(i);this.stack=h.stack},t.DOMException.prototype=Object.create(Error.prototype),t.DOMException.prototype.constructor=t.DOMException}function I(n,i){return new Promise(function(l,h){var p=new S(n,i);if(p.signal&&p.signal.aborted)return h(new t.DOMException("Aborted","AbortError"));var d=new XMLHttpRequest;function U(){d.abort()}d.onload=function(){var b={statusText:d.statusText,headers:te(d.getAllResponseHeaders()||"")};p.url.indexOf("file://")===0&&(d.status<200||d.status>599)?b.status=200:b.status=d.status,b.url="responseURL"in d?d.responseURL:b.headers.get("X-Request-URL");var R="response"in d?d.response:d.responseText;setTimeout(function(){l(new E(R,b))},0)},d.onerror=function(){setTimeout(function(){h(new TypeError("Network request failed"))},0)},d.ontimeout=function(){setTimeout(function(){h(new TypeError("Network request timed out"))},0)},d.onabort=function(){setTimeout(function(){h(new t.DOMException("Aborted","AbortError"))},0)};function A(b){try{return b===""&&s.location.href?s.location.href:b}catch{return b}}if(d.open(p.method,A(p.url),!0),p.credentials==="include"?d.withCredentials=!0:p.credentials==="omit"&&(d.withCredentials=!1),"responseType"in d&&(a.blob?d.responseType="blob":a.arrayBuffer&&(d.responseType="arraybuffer")),i&&typeof i.headers=="object"&&!(i.headers instanceof g||s.Headers&&i.headers instanceof s.Headers)){var H=[];Object.getOwnPropertyNames(i.headers).forEach(function(b){H.push(f(b)),d.setRequestHeader(b,y(i.headers[b]))}),p.headers.forEach(function(b,R){H.indexOf(R)===-1&&d.setRequestHeader(R,b)})}else p.headers.forEach(function(b,R){d.setRequestHeader(R,b)});p.signal&&(p.signal.addEventListener("abort",U),d.onreadystatechange=function(){d.readyState===4&&p.signal.removeEventListener("abort",U)}),d.send(typeof p._bodyInit>"u"?null:p._bodyInit)})}return I.polyfill=!0,s.fetch||(s.fetch=I,s.Headers=g,s.Request=S,s.Response=E),t.Headers=g,t.Request=S,t.Response=E,t.fetch=I,t})({})})(typeof self<"u"?self:Q)});var v=[];for(let r=0;r<256;++r)v.push((r+256).toString(16).slice(1));function K(r,e=0){return(v[r[e+0]]+v[r[e+1]]+v[r[e+2]]+v[r[e+3]]+"-"+v[r[e+4]]+v[r[e+5]]+"-"+v[r[e+6]]+v[r[e+7]]+"-"+v[r[e+8]]+v[r[e+9]]+"-"+v[r[e+10]]+v[r[e+11]]+v[r[e+12]]+v[r[e+13]]+v[r[e+14]]+v[r[e+15]]).toLowerCase()}var P,de=new Uint8Array(16);function B(){if(!P){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");P=crypto.getRandomValues.bind(crypto)}return P(de)}var fe=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),L={randomUUID:fe};function he(r,e,t){r=r||{};let s=r.random??r.rng?.()??B();if(s.length<16)throw new Error("Random bytes length must be >= 16");if(s[6]=s[6]&15|64,s[8]=s[8]&63|128,e){if(t=t||0,t<0||t+16>e.length)throw new RangeError(`UUID byte range ${t}:${t+15} is out of buffer bounds`);for(let a=0;a<16;++a)e[t+a]=s[a];return e}return K(s)}function ge(r,e,t){return L.randomUUID&&!e&&!r?L.randomUUID():he(r,e,t)}var _=ge;var st=ce(z());function w(r){return pe(r,!1)}function pe(r,e){return r==null?r:{companyId:r.company_id==null?void 0:r.company_id,error:r.error==null?void 0:r.error,featureAllocation:r.feature_allocation==null?void 0:r.feature_allocation,featureUsage:r.feature_usage==null?void 0:r.feature_usage,featureUsageEvent:r.feature_usage_event==null?void 0:r.feature_usage_event,featureUsagePeriod:r.feature_usage_period==null?void 0:r.feature_usage_period,featureUsageResetAt:r.feature_usage_reset_at==null?void 0:new Date(r.feature_usage_reset_at),flag:r.flag,flagId:r.flag_id==null?void 0:r.flag_id,reason:r.reason,ruleId:r.rule_id==null?void 0:r.rule_id,ruleType:r.rule_type==null?void 0:r.rule_type,userId:r.user_id==null?void 0:r.user_id,value:r.value}}function N(r){return ye(r,!1)}function ye(r,e=!1){return r==null?r:{company_id:r.companyId,error:r.error,flag_id:r.flagId,flag_key:r.flagKey,reason:r.reason,req_company:r.reqCompany,req_user:r.reqUser,rule_id:r.ruleId,user_id:r.userId,value:r.value}}function T(r){return be(r,!1)}function be(r,e){return r==null?r:{data:w(r.data),params:r.params}}function X(r){return ve(r,!1)}function ve(r,e){return r==null?r:{flags:r.flags.map(w)}}function J(r){return ke(r,!1)}function ke(r,e){return r==null?r:{data:X(r.data),params:r.params}}var O=r=>{let{companyId:e,error:t,featureAllocation:s,featureUsage:a,featureUsageEvent:c,featureUsagePeriod:o,featureUsageResetAt:u,flag:f,flagId:y,reason:F,ruleId:g,ruleType:k,userId:m,value:x}=w(r);return{featureUsageExceeded:!x&&(k=="company_override_usage_exceeded"||k=="plan_entitlement_usage_exceeded"),companyId:e??void 0,error:t??void 0,featureAllocation:s??void 0,featureUsage:a??void 0,featureUsageEvent:c===null?void 0:c,featureUsagePeriod:o??void 0,featureUsageResetAt:u??void 0,flag:f,flagId:y??void 0,reason:F,ruleId:g??void 0,ruleType:k??void 0,userId:m??void 0,value:x}};function C(r){let e=Object.keys(r).reduce((t,s)=>{let c=Object.keys(r[s]||{}).sort().reduce((o,u)=>(o[u]=r[s][u],o),{});return t[s]=c,t},{});return JSON.stringify(e)}var M="1.2.5";var G="schematicId";var D=class{additionalHeaders={};apiKey;apiUrl="https://api.schematichq.com";conn=null;context={};debugEnabled=!1;offlineEnabled=!1;eventQueue;contextDependentEventQueue;eventUrl="https://c.schematichq.com";flagCheckListeners={};flagValueListeners={};isPending=!0;isPendingListeners=new Set;storage;useWebSocket=!1;checks={};featureUsageEventMap={};webSocketUrl="wss://api.schematichq.com";constructor(e,t){if(this.apiKey=e,this.eventQueue=[],this.contextDependentEventQueue=[],this.useWebSocket=t?.useWebSocket??!1,this.debugEnabled=t?.debug??!1,this.offlineEnabled=t?.offline??!1,typeof window<"u"&&typeof window.location<"u"){let s=new URLSearchParams(window.location.search),a=s.get("schematic_debug");a!==null&&(a===""||a==="true"||a==="1")&&(this.debugEnabled=!0);let c=s.get("schematic_offline");c!==null&&(c===""||c==="true"||c==="1")&&(this.offlineEnabled=!0,this.debugEnabled=!0)}this.offlineEnabled&&t?.debug!==!1&&(this.debugEnabled=!0),this.offlineEnabled&&this.setIsPending(!1),this.additionalHeaders={"X-Schematic-Client-Version":`schematic-js@${M}`,...t?.additionalHeaders??{}},t?.storage?this.storage=t.storage:typeof localStorage<"u"&&(this.storage=localStorage),t?.apiUrl!==void 0&&(this.apiUrl=t.apiUrl),t?.eventUrl!==void 0&&(this.eventUrl=t.eventUrl),t?.webSocketUrl!==void 0&&(this.webSocketUrl=t.webSocketUrl),typeof window<"u"&&window?.addEventListener&&window.addEventListener("beforeunload",()=>{this.flushEventQueue(),this.flushContextDependentEventQueue()}),this.offlineEnabled?this.debug("Initialized with offline mode enabled - no network requests will be made"):this.debugEnabled&&this.debug("Initialized with debug mode enabled")}async checkFlag(e){let{fallback:t=!1,key:s}=e,a=e.context||this.context,c=C(a);if(this.debug(`checkFlag: ${s}`,{context:a,fallback:t}),this.isOffline())return this.debug(`checkFlag offline result: ${s}`,{value:t,offlineMode:!0}),t;if(!this.useWebSocket){let o=`${this.apiUrl}/flags/${s}/check`;return fetch(o,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(a)}).then(u=>{if(!u.ok)throw new Error("Network response was not ok");return u.json()}).then(u=>{let f=T(u);this.debug(`checkFlag result: ${s}`,f);let y=O(f.data);return typeof y.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(y),this.submitFlagCheckEvent(s,y,a),y.value}).catch(u=>{console.error("There was a problem with the fetch operation:",u);let f={flag:s,value:t,reason:"API request failed",error:u instanceof Error?u.message:String(u)};return this.submitFlagCheckEvent(s,f,a),t})}try{let o=this.checks[c];if(this.conn!==null&&typeof o<"u"&&typeof o[s]<"u")return this.debug(`checkFlag cached result: ${s}`,o[s]),o[s].value;if(this.isOffline())return t;try{await this.setContext(a)}catch(F){return console.error("WebSocket connection failed, falling back to REST:",F),this.fallbackToRest(s,a,t)}let f=(this.checks[c]??{})[s],y=f?.value??t;return this.debug(`checkFlag WebSocket result: ${s}`,typeof f<"u"?f:{value:t,fallbackUsed:!0}),typeof f<"u"&&this.submitFlagCheckEvent(s,f,a),y}catch(o){console.error("Unexpected error in checkFlag:",o);let u={flag:s,value:t,reason:"Unexpected error in flag check",error:o instanceof Error?o.message:String(o)};return this.submitFlagCheckEvent(s,u,a),t}}debug(e,...t){this.debugEnabled&&console.log(`[Schematic] ${e}`,...t)}isOffline(){return this.offlineEnabled}submitFlagCheckEvent(e,t,s){let a={flagKey:e,value:t.value,reason:t.reason,flagId:t.flagId,ruleId:t.ruleId,companyId:t.companyId,userId:t.userId,error:t.error,reqCompany:s.company,reqUser:s.user};return this.debug("submitting flag check event:",a),this.handleEvent("flag_check",N(a))}async fallbackToRest(e,t,s){if(this.isOffline())return this.debug(`fallbackToRest offline result: ${e}`,{value:s,offlineMode:!0}),s;try{let a=`${this.apiUrl}/flags/${e}/check`,c=await fetch(a,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(t)});if(!c.ok)throw new Error("Network response was not ok");let o=await c.json(),u=T(o);this.debug(`fallbackToRest result: ${e}`,u);let f=O(u.data);return typeof f.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(f),this.submitFlagCheckEvent(e,f,t),f.value}catch(a){console.error("REST API call failed, using fallback value:",a);let c={flag:e,value:s,reason:"API request failed (fallback)",error:a instanceof Error?a.message:String(a)};return this.submitFlagCheckEvent(e,c,t),s}}checkFlags=async e=>{if(e=e||this.context,this.debug("checkFlags",{context:e}),this.isOffline())return this.debug("checkFlags offline result: returning empty object"),{};let t=`${this.apiUrl}/flags/check`,s=JSON.stringify(e);return fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:s}).then(a=>{if(!a.ok)throw new Error("Network response was not ok");return a.json()}).then(a=>{let c=J(a);return this.debug("checkFlags result:",c),(c?.data?.flags??[]).reduce((o,u)=>(o[u.flag]=u.value,o),{})}).catch(a=>(console.error("There was a problem with the fetch operation:",a),{}))};identify=e=>{this.debug("identify:",e);try{this.setContext({company:e.company?.keys,user:e.keys})}catch(t){console.error("Error setting context:",t)}return this.handleEvent("identify",e)};setContext=async e=>{if(this.isOffline()||!this.useWebSocket)return this.context=e,this.flushContextDependentEventQueue(),this.setIsPending(!1),Promise.resolve();try{this.setIsPending(!0),this.conn||(this.conn=this.wsConnect());let t=await this.conn;await this.wsSendMessage(t,e)}catch(t){throw console.error("Failed to establish WebSocket connection:",t),t}};track=e=>{let{company:t,user:s,event:a,traits:c,quantity:o=1}=e;if(!this.hasContext(t,s)){this.debug(`track: queuing event "${a}" until context is available`);let f={api_key:this.apiKey,body:{company:t,event:a,traits:c??{},user:s,quantity:o},sent_at:new Date().toISOString(),tracker_event_id:_(),tracker_user_id:this.getAnonymousId(),type:"track"};return this.contextDependentEventQueue.push(f),Promise.resolve()}let u={company:t??this.context.company,event:a,traits:c??{},user:s??this.context.user,quantity:o};return this.debug("track:",u),a in this.featureUsageEventMap&&this.optimisticallyUpdateFeatureUsage(a,o),this.handleEvent("track",u)};optimisticallyUpdateFeatureUsage=(e,t=1)=>{let s=this.featureUsageEventMap[e];s!=null&&(this.debug(`Optimistically updating feature usage for event: ${e}`,{quantity:t}),Object.entries(s).forEach(([a,c])=>{if(c===void 0)return;let o={...c};if(typeof o.featureUsage=="number"){if(o.featureUsage+=t,typeof o.featureAllocation=="number"){let f=o.featureUsageExceeded===!0,y=o.featureUsage>=o.featureAllocation;y!==f&&(o.featureUsageExceeded=y,y&&(o.value=!1),this.debug(`Usage limit status changed for flag: ${a}`,{was:f?"exceeded":"within limits",now:y?"exceeded":"within limits",featureUsage:o.featureUsage,featureAllocation:o.featureAllocation,value:o.value}))}this.featureUsageEventMap[e]!==void 0&&(this.featureUsageEventMap[e][a]=o);let u=C(this.context);this.checks[u]!==void 0&&this.checks[u]!==null&&(this.checks[u][a]=o),this.notifyFlagCheckListeners(a,o),this.notifyFlagValueListeners(a,o.value)}}))};hasContext=(e,t)=>{let s=e!=null&&Object.keys(e).length>0||t!=null&&Object.keys(t).length>0,a=this.context.company!==void 0&&this.context.company!==null&&Object.keys(this.context.company).length>0||this.context.user!==void 0&&this.context.user!==null&&Object.keys(this.context.user).length>0;return s||a};flushContextDependentEventQueue=()=>{for(this.debug(`flushing ${this.contextDependentEventQueue.length} context-dependent events`);this.contextDependentEventQueue.length>0;){let e=this.contextDependentEventQueue.shift();if(e)if(e.type==="track"&&typeof e.body=="object"&&e.body!==null){let t=e.body,s={...t,company:t.company??this.context.company,user:t.user??this.context.user},a={...e,body:s,sent_at:new Date().toISOString()};this.sendEvent(a)}else this.sendEvent(e)}};flushEventQueue=()=>{for(;this.eventQueue.length>0;){let e=this.eventQueue.shift();e&&this.sendEvent(e)}};getAnonymousId=()=>{if(!this.storage)return _();let e=this.storage.getItem(G);if(typeof e<"u")return e;let t=_();return this.storage.setItem(G,t),t};handleEvent=(e,t)=>{let s={api_key:this.apiKey,body:t,sent_at:new Date().toISOString(),tracker_event_id:_(),tracker_user_id:this.getAnonymousId(),type:e};return document?.hidden?this.storeEvent(s):this.sendEvent(s)};sendEvent=async e=>{let t=`${this.eventUrl}/e`,s=JSON.stringify(e);if(this.debug("sending event:",{url:t,event:e}),this.isOffline())return this.debug("event not sent (offline mode):",{event:e}),Promise.resolve();try{let a=await fetch(t,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:s});this.debug("event sent:",{status:a.status,statusText:a.statusText})}catch(a){console.error("Error sending Schematic event: ",a)}return Promise.resolve()};storeEvent=e=>(this.eventQueue.push(e),Promise.resolve());cleanup=async()=>{if(this.isOffline())return this.debug("cleanup: skipped (offline mode)"),Promise.resolve();if(this.conn)try{(await this.conn).close()}catch(e){console.error("Error during cleanup:",e)}finally{this.conn=null}};wsConnect=()=>this.isOffline()?(this.debug("wsConnect: skipped (offline mode)"),Promise.reject(new Error("WebSocket connection skipped in offline mode"))):new Promise((e,t)=>{let s=`${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;this.debug("connecting to WebSocket:",s);let a=new WebSocket(s);a.onopen=()=>{this.debug("WebSocket connection opened"),e(a)},a.onerror=c=>{this.debug("WebSocket connection error:",c),t(c)},a.onclose=()=>{this.debug("WebSocket connection closed"),this.conn=null}});wsSendMessage=(e,t)=>this.isOffline()?(this.debug("wsSendMessage: skipped (offline mode)"),this.setIsPending(!1),Promise.resolve()):new Promise((s,a)=>{if(C(t)==C(this.context))return this.debug("WebSocket context unchanged, skipping update"),s(this.setIsPending(!1));this.debug("WebSocket context updated:",t),this.context=t;let c=()=>{let o=!1,u=F=>{let g=JSON.parse(F.data);this.debug("WebSocket message received:",g),C(t)in this.checks||(this.checks[C(t)]={}),(g.flags??[]).forEach(k=>{let m=O(k),x=C(t);this.checks[x]===void 0&&(this.checks[x]={}),this.checks[x][m.flag]=m,this.debug("WebSocket flag update:",{flag:m.flag,value:m.value,flagCheck:m}),typeof m.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(m),(this.flagCheckListeners[k.flag]?.size>0||this.flagValueListeners[k.flag]?.size>0)&&this.submitFlagCheckEvent(m.flag,m,t),this.notifyFlagCheckListeners(k.flag,m),this.notifyFlagValueListeners(k.flag,m.value)}),this.flushContextDependentEventQueue(),this.setIsPending(!1),o||(o=!0,s())};e.addEventListener("message",u);let f=this.additionalHeaders["X-Schematic-Client-Version"]??`schematic-js@${M}`,y={apiKey:this.apiKey,clientVersion:f,data:t};this.debug("WebSocket sending message:",y),e.send(JSON.stringify(y))};e.readyState===WebSocket.OPEN?(this.debug("WebSocket already open, sending message"),c()):e.readyState===WebSocket.CONNECTING?(this.debug("WebSocket connecting, waiting for open to send message"),e.addEventListener("open",c)):(this.debug("WebSocket is closed, cannot send message"),a("WebSocket is not open or connecting"))});getIsPending=()=>this.isPending;addIsPendingListener=e=>(this.isPendingListeners.add(e),()=>{this.isPendingListeners.delete(e)});setIsPending=e=>{this.isPending=e,this.isPendingListeners.forEach(t=>Ee(t,e))};getFlagCheck=e=>{let t=C(this.context);return(this.checks[t]??{})[e]};getFlagValue=e=>this.getFlagCheck(e)?.value;addFlagValueListener=(e,t)=>(e in this.flagValueListeners||(this.flagValueListeners[e]=new Set),this.flagValueListeners[e].add(t),()=>{this.flagValueListeners[e].delete(t)});addFlagCheckListener=(e,t)=>(e in this.flagCheckListeners||(this.flagCheckListeners[e]=new Set),this.flagCheckListeners[e].add(t),()=>{this.flagCheckListeners[e].delete(t)});notifyFlagCheckListeners=(e,t)=>{let s=this.flagCheckListeners?.[e]??[];s.size>0&&this.debug(`Notifying ${s.size} flag check listeners for ${e}`,t),typeof t.featureUsageEvent=="string"&&this.updateFeatureUsageEventMap(t),s.forEach(a=>Fe(a,t))};updateFeatureUsageEventMap=e=>{if(typeof e.featureUsageEvent!="string")return;let t=e.featureUsageEvent;(this.featureUsageEventMap[t]===void 0||this.featureUsageEventMap[t]===null)&&(this.featureUsageEventMap[t]={}),this.featureUsageEventMap[t]!==void 0&&(this.featureUsageEventMap[t][e.flag]=e),this.debug(`Updated featureUsageEventMap for event: ${t}, flag: ${e.flag}`,e)};notifyFlagValueListeners=(e,t)=>{let s=this.flagValueListeners?.[e]??[];s.size>0&&this.debug(`Notifying ${s.size} flag value listeners for ${e}`,{value:t}),s.forEach(a=>Ce(a,t))}},Ee=(r,e)=>{r.length>0?r(e):r()},Fe=(r,e)=>{r.length>0?r(e):r()},Ce=(r,e)=>{r.length>0?r(e):r()};window.Schematic=D;})();
|
|
3
3
|
/* @preserve */
|
package/dist/schematic.cjs.js
CHANGED
|
@@ -34,20 +34,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
34
34
|
var require_browser_polyfill = __commonJS({
|
|
35
35
|
"node_modules/cross-fetch/dist/browser-polyfill.js"(exports) {
|
|
36
36
|
(function(self2) {
|
|
37
|
-
var irrelevant = function(exports2) {
|
|
37
|
+
var irrelevant = (function(exports2) {
|
|
38
38
|
var g = typeof globalThis !== "undefined" && globalThis || typeof self2 !== "undefined" && self2 || // eslint-disable-next-line no-undef
|
|
39
39
|
typeof global !== "undefined" && global || {};
|
|
40
40
|
var support = {
|
|
41
41
|
searchParams: "URLSearchParams" in g,
|
|
42
42
|
iterable: "Symbol" in g && "iterator" in Symbol,
|
|
43
|
-
blob: "FileReader" in g && "Blob" in g && function() {
|
|
43
|
+
blob: "FileReader" in g && "Blob" in g && (function() {
|
|
44
44
|
try {
|
|
45
45
|
new Blob();
|
|
46
46
|
return true;
|
|
47
47
|
} catch (e) {
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
|
-
}(),
|
|
50
|
+
})(),
|
|
51
51
|
formData: "FormData" in g,
|
|
52
52
|
arrayBuffer: "ArrayBuffer" in g
|
|
53
53
|
};
|
|
@@ -349,12 +349,12 @@ var require_browser_polyfill = __commonJS({
|
|
|
349
349
|
}
|
|
350
350
|
this.method = normalizeMethod(options.method || this.method || "GET");
|
|
351
351
|
this.mode = options.mode || this.mode || null;
|
|
352
|
-
this.signal = options.signal || this.signal || function() {
|
|
352
|
+
this.signal = options.signal || this.signal || (function() {
|
|
353
353
|
if ("AbortController" in g) {
|
|
354
354
|
var ctrl = new AbortController();
|
|
355
355
|
return ctrl.signal;
|
|
356
356
|
}
|
|
357
|
-
}();
|
|
357
|
+
})();
|
|
358
358
|
this.referrer = null;
|
|
359
359
|
if ((this.method === "GET" || this.method === "HEAD") && body) {
|
|
360
360
|
throw new TypeError("Body not allowed for GET or HEAD requests");
|
|
@@ -561,7 +561,7 @@ var require_browser_polyfill = __commonJS({
|
|
|
561
561
|
exports2.Response = Response;
|
|
562
562
|
exports2.fetch = fetch2;
|
|
563
563
|
return exports2;
|
|
564
|
-
}({});
|
|
564
|
+
})({});
|
|
565
565
|
})(typeof self !== "undefined" ? self : exports);
|
|
566
566
|
}
|
|
567
567
|
});
|
|
@@ -579,7 +579,7 @@ __export(index_exports, {
|
|
|
579
579
|
});
|
|
580
580
|
module.exports = __toCommonJS(index_exports);
|
|
581
581
|
|
|
582
|
-
// node_modules/uuid/dist/
|
|
582
|
+
// node_modules/uuid/dist/stringify.js
|
|
583
583
|
var byteToHex = [];
|
|
584
584
|
for (let i = 0; i < 256; ++i) {
|
|
585
585
|
byteToHex.push((i + 256).toString(16).slice(1));
|
|
@@ -588,7 +588,7 @@ function unsafeStringify(arr, offset = 0) {
|
|
|
588
588
|
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
-
// node_modules/uuid/dist/
|
|
591
|
+
// node_modules/uuid/dist/rng.js
|
|
592
592
|
var getRandomValues;
|
|
593
593
|
var rnds8 = new Uint8Array(16);
|
|
594
594
|
function rng() {
|
|
@@ -601,15 +601,12 @@ function rng() {
|
|
|
601
601
|
return getRandomValues(rnds8);
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
// node_modules/uuid/dist/
|
|
604
|
+
// node_modules/uuid/dist/native.js
|
|
605
605
|
var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
|
|
606
606
|
var native_default = { randomUUID };
|
|
607
607
|
|
|
608
|
-
// node_modules/uuid/dist/
|
|
609
|
-
function
|
|
610
|
-
if (native_default.randomUUID && !buf && !options) {
|
|
611
|
-
return native_default.randomUUID();
|
|
612
|
-
}
|
|
608
|
+
// node_modules/uuid/dist/v4.js
|
|
609
|
+
function _v4(options, buf, offset) {
|
|
613
610
|
options = options || {};
|
|
614
611
|
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
615
612
|
if (rnds.length < 16) {
|
|
@@ -629,6 +626,12 @@ function v4(options, buf, offset) {
|
|
|
629
626
|
}
|
|
630
627
|
return unsafeStringify(rnds);
|
|
631
628
|
}
|
|
629
|
+
function v4(options, buf, offset) {
|
|
630
|
+
if (native_default.randomUUID && !buf && !options) {
|
|
631
|
+
return native_default.randomUUID();
|
|
632
|
+
}
|
|
633
|
+
return _v4(options, buf, offset);
|
|
634
|
+
}
|
|
632
635
|
var v4_default = v4;
|
|
633
636
|
|
|
634
637
|
// src/index.ts
|
|
@@ -647,6 +650,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
647
650
|
error: json["error"] == null ? void 0 : json["error"],
|
|
648
651
|
featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
|
|
649
652
|
featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
|
|
653
|
+
featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
|
|
650
654
|
featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
|
|
651
655
|
featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
|
|
652
656
|
flag: json["flag"],
|
|
@@ -746,6 +750,7 @@ var CheckFlagReturnFromJSON = (json) => {
|
|
|
746
750
|
error,
|
|
747
751
|
featureAllocation,
|
|
748
752
|
featureUsage,
|
|
753
|
+
featureUsageEvent,
|
|
749
754
|
featureUsagePeriod,
|
|
750
755
|
featureUsageResetAt,
|
|
751
756
|
flag,
|
|
@@ -765,6 +770,7 @@ var CheckFlagReturnFromJSON = (json) => {
|
|
|
765
770
|
error: error == null ? void 0 : error,
|
|
766
771
|
featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
|
|
767
772
|
featureUsage: featureUsage == null ? void 0 : featureUsage,
|
|
773
|
+
featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
|
|
768
774
|
featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
|
|
769
775
|
featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
|
|
770
776
|
flag,
|
|
@@ -794,7 +800,7 @@ function contextString(context) {
|
|
|
794
800
|
}
|
|
795
801
|
|
|
796
802
|
// src/version.ts
|
|
797
|
-
var version = "1.2.
|
|
803
|
+
var version = "1.2.5";
|
|
798
804
|
|
|
799
805
|
// src/index.ts
|
|
800
806
|
var anonymousIdKey = "schematicId";
|
|
@@ -807,6 +813,7 @@ var Schematic = class {
|
|
|
807
813
|
debugEnabled = false;
|
|
808
814
|
offlineEnabled = false;
|
|
809
815
|
eventQueue;
|
|
816
|
+
contextDependentEventQueue;
|
|
810
817
|
eventUrl = "https://c.schematichq.com";
|
|
811
818
|
flagCheckListeners = {};
|
|
812
819
|
flagValueListeners = {};
|
|
@@ -815,10 +822,12 @@ var Schematic = class {
|
|
|
815
822
|
storage;
|
|
816
823
|
useWebSocket = false;
|
|
817
824
|
checks = {};
|
|
825
|
+
featureUsageEventMap = {};
|
|
818
826
|
webSocketUrl = "wss://api.schematichq.com";
|
|
819
827
|
constructor(apiKey, options) {
|
|
820
828
|
this.apiKey = apiKey;
|
|
821
829
|
this.eventQueue = [];
|
|
830
|
+
this.contextDependentEventQueue = [];
|
|
822
831
|
this.useWebSocket = options?.useWebSocket ?? false;
|
|
823
832
|
this.debugEnabled = options?.debug ?? false;
|
|
824
833
|
this.offlineEnabled = options?.offline ?? false;
|
|
@@ -861,6 +870,7 @@ var Schematic = class {
|
|
|
861
870
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
862
871
|
window.addEventListener("beforeunload", () => {
|
|
863
872
|
this.flushEventQueue();
|
|
873
|
+
this.flushContextDependentEventQueue();
|
|
864
874
|
});
|
|
865
875
|
}
|
|
866
876
|
if (this.offlineEnabled) {
|
|
@@ -909,6 +919,9 @@ var Schematic = class {
|
|
|
909
919
|
const parsedResponse = CheckFlagResponseFromJSON(response);
|
|
910
920
|
this.debug(`checkFlag result: ${key}`, parsedResponse);
|
|
911
921
|
const result = CheckFlagReturnFromJSON(parsedResponse.data);
|
|
922
|
+
if (typeof result.featureUsageEvent === "string") {
|
|
923
|
+
this.updateFeatureUsageEventMap(result);
|
|
924
|
+
}
|
|
912
925
|
this.submitFlagCheckEvent(key, result, context);
|
|
913
926
|
return result.value;
|
|
914
927
|
}).catch((error) => {
|
|
@@ -1028,6 +1041,9 @@ var Schematic = class {
|
|
|
1028
1041
|
const data = CheckFlagResponseFromJSON(responseJson);
|
|
1029
1042
|
this.debug(`fallbackToRest result: ${key}`, data);
|
|
1030
1043
|
const result = CheckFlagReturnFromJSON(data.data);
|
|
1044
|
+
if (typeof result.featureUsageEvent === "string") {
|
|
1045
|
+
this.updateFeatureUsageEventMap(result);
|
|
1046
|
+
}
|
|
1031
1047
|
this.submitFlagCheckEvent(key, result, context);
|
|
1032
1048
|
return result.value;
|
|
1033
1049
|
} catch (error) {
|
|
@@ -1111,14 +1127,12 @@ var Schematic = class {
|
|
|
1111
1127
|
* In offline mode, this will just set the context locally without connecting.
|
|
1112
1128
|
*/
|
|
1113
1129
|
setContext = async (context) => {
|
|
1114
|
-
if (this.isOffline()) {
|
|
1130
|
+
if (this.isOffline() || !this.useWebSocket) {
|
|
1115
1131
|
this.context = context;
|
|
1132
|
+
this.flushContextDependentEventQueue();
|
|
1116
1133
|
this.setIsPending(false);
|
|
1117
1134
|
return Promise.resolve();
|
|
1118
1135
|
}
|
|
1119
|
-
if (!this.useWebSocket) {
|
|
1120
|
-
return Promise.resolve();
|
|
1121
|
-
}
|
|
1122
1136
|
try {
|
|
1123
1137
|
this.setIsPending(true);
|
|
1124
1138
|
if (!this.conn) {
|
|
@@ -1134,21 +1148,123 @@ var Schematic = class {
|
|
|
1134
1148
|
/**
|
|
1135
1149
|
* Send a track event
|
|
1136
1150
|
* Track usage for a company and/or user.
|
|
1151
|
+
* Optimistically updates feature usage flags if tracking a featureUsageEvent.
|
|
1137
1152
|
*/
|
|
1138
1153
|
track = (body) => {
|
|
1139
|
-
const { company, user, event, traits } = body;
|
|
1154
|
+
const { company, user, event, traits, quantity = 1 } = body;
|
|
1155
|
+
if (!this.hasContext(company, user)) {
|
|
1156
|
+
this.debug(`track: queuing event "${event}" until context is available`);
|
|
1157
|
+
const queuedEvent = {
|
|
1158
|
+
api_key: this.apiKey,
|
|
1159
|
+
body: {
|
|
1160
|
+
company,
|
|
1161
|
+
event,
|
|
1162
|
+
traits: traits ?? {},
|
|
1163
|
+
user,
|
|
1164
|
+
quantity
|
|
1165
|
+
},
|
|
1166
|
+
sent_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1167
|
+
tracker_event_id: v4_default(),
|
|
1168
|
+
tracker_user_id: this.getAnonymousId(),
|
|
1169
|
+
type: "track"
|
|
1170
|
+
};
|
|
1171
|
+
this.contextDependentEventQueue.push(queuedEvent);
|
|
1172
|
+
return Promise.resolve();
|
|
1173
|
+
}
|
|
1140
1174
|
const trackData = {
|
|
1141
1175
|
company: company ?? this.context.company,
|
|
1142
1176
|
event,
|
|
1143
1177
|
traits: traits ?? {},
|
|
1144
|
-
user: user ?? this.context.user
|
|
1178
|
+
user: user ?? this.context.user,
|
|
1179
|
+
quantity
|
|
1145
1180
|
};
|
|
1146
1181
|
this.debug(`track:`, trackData);
|
|
1182
|
+
if (event in this.featureUsageEventMap) {
|
|
1183
|
+
this.optimisticallyUpdateFeatureUsage(event, quantity);
|
|
1184
|
+
}
|
|
1147
1185
|
return this.handleEvent("track", trackData);
|
|
1148
1186
|
};
|
|
1187
|
+
/**
|
|
1188
|
+
* Optimistically update feature usage flags associated with a tracked event
|
|
1189
|
+
* This updates flags in memory with updated usage counts and value/featureUsageExceeded flags
|
|
1190
|
+
* before the network request completes
|
|
1191
|
+
*/
|
|
1192
|
+
optimisticallyUpdateFeatureUsage = (eventName, quantity = 1) => {
|
|
1193
|
+
const flagsForEvent = this.featureUsageEventMap[eventName];
|
|
1194
|
+
if (flagsForEvent === void 0 || flagsForEvent === null) return;
|
|
1195
|
+
this.debug(
|
|
1196
|
+
`Optimistically updating feature usage for event: ${eventName}`,
|
|
1197
|
+
{ quantity }
|
|
1198
|
+
);
|
|
1199
|
+
Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
|
|
1200
|
+
if (check === void 0) return;
|
|
1201
|
+
const updatedCheck = { ...check };
|
|
1202
|
+
if (typeof updatedCheck.featureUsage === "number") {
|
|
1203
|
+
updatedCheck.featureUsage += quantity;
|
|
1204
|
+
if (typeof updatedCheck.featureAllocation === "number") {
|
|
1205
|
+
const wasExceeded = updatedCheck.featureUsageExceeded === true;
|
|
1206
|
+
const nowExceeded = updatedCheck.featureUsage >= updatedCheck.featureAllocation;
|
|
1207
|
+
if (nowExceeded !== wasExceeded) {
|
|
1208
|
+
updatedCheck.featureUsageExceeded = nowExceeded;
|
|
1209
|
+
if (nowExceeded) {
|
|
1210
|
+
updatedCheck.value = false;
|
|
1211
|
+
}
|
|
1212
|
+
this.debug(`Usage limit status changed for flag: ${flagKey}`, {
|
|
1213
|
+
was: wasExceeded ? "exceeded" : "within limits",
|
|
1214
|
+
now: nowExceeded ? "exceeded" : "within limits",
|
|
1215
|
+
featureUsage: updatedCheck.featureUsage,
|
|
1216
|
+
featureAllocation: updatedCheck.featureAllocation,
|
|
1217
|
+
value: updatedCheck.value
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (this.featureUsageEventMap[eventName] !== void 0) {
|
|
1222
|
+
this.featureUsageEventMap[eventName][flagKey] = updatedCheck;
|
|
1223
|
+
}
|
|
1224
|
+
const contextStr = contextString(this.context);
|
|
1225
|
+
if (this.checks[contextStr] !== void 0 && this.checks[contextStr] !== null) {
|
|
1226
|
+
this.checks[contextStr][flagKey] = updatedCheck;
|
|
1227
|
+
}
|
|
1228
|
+
this.notifyFlagCheckListeners(flagKey, updatedCheck);
|
|
1229
|
+
this.notifyFlagValueListeners(flagKey, updatedCheck.value);
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
};
|
|
1149
1233
|
/**
|
|
1150
1234
|
* Event processing
|
|
1151
1235
|
*/
|
|
1236
|
+
hasContext = (company, user) => {
|
|
1237
|
+
const hasProvidedContext = company !== void 0 && company !== null && Object.keys(company).length > 0 || user !== void 0 && user !== null && Object.keys(user).length > 0;
|
|
1238
|
+
const hasInstanceContext = this.context.company !== void 0 && this.context.company !== null && Object.keys(this.context.company).length > 0 || this.context.user !== void 0 && this.context.user !== null && Object.keys(this.context.user).length > 0;
|
|
1239
|
+
return hasProvidedContext || hasInstanceContext;
|
|
1240
|
+
};
|
|
1241
|
+
flushContextDependentEventQueue = () => {
|
|
1242
|
+
this.debug(
|
|
1243
|
+
`flushing ${this.contextDependentEventQueue.length} context-dependent events`
|
|
1244
|
+
);
|
|
1245
|
+
while (this.contextDependentEventQueue.length > 0) {
|
|
1246
|
+
const event = this.contextDependentEventQueue.shift();
|
|
1247
|
+
if (event) {
|
|
1248
|
+
if (event.type === "track" && typeof event.body === "object" && event.body !== null) {
|
|
1249
|
+
const trackBody = event.body;
|
|
1250
|
+
const updatedBody = {
|
|
1251
|
+
...trackBody,
|
|
1252
|
+
company: trackBody.company ?? this.context.company,
|
|
1253
|
+
user: trackBody.user ?? this.context.user
|
|
1254
|
+
};
|
|
1255
|
+
const updatedEvent = {
|
|
1256
|
+
...event,
|
|
1257
|
+
body: updatedBody,
|
|
1258
|
+
sent_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1259
|
+
// Update timestamp to actual send time
|
|
1260
|
+
};
|
|
1261
|
+
this.sendEvent(updatedEvent);
|
|
1262
|
+
} else {
|
|
1263
|
+
this.sendEvent(event);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1152
1268
|
flushEventQueue = () => {
|
|
1153
1269
|
while (this.eventQueue.length > 0) {
|
|
1154
1270
|
const event = this.eventQueue.shift();
|
|
@@ -1288,18 +1404,26 @@ var Schematic = class {
|
|
|
1288
1404
|
}
|
|
1289
1405
|
(message.flags ?? []).forEach((flag) => {
|
|
1290
1406
|
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1291
|
-
|
|
1407
|
+
const contextStr = contextString(context);
|
|
1408
|
+
if (this.checks[contextStr] === void 0) {
|
|
1409
|
+
this.checks[contextStr] = {};
|
|
1410
|
+
}
|
|
1411
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1292
1412
|
this.debug(`WebSocket flag update:`, {
|
|
1293
1413
|
flag: flagCheck.flag,
|
|
1294
1414
|
value: flagCheck.value,
|
|
1295
1415
|
flagCheck
|
|
1296
1416
|
});
|
|
1417
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1418
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1419
|
+
}
|
|
1297
1420
|
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1298
1421
|
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1299
1422
|
}
|
|
1300
1423
|
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1301
1424
|
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1302
1425
|
});
|
|
1426
|
+
this.flushContextDependentEventQueue();
|
|
1303
1427
|
this.setIsPending(false);
|
|
1304
1428
|
if (!resolved) {
|
|
1305
1429
|
resolved = true;
|
|
@@ -1386,8 +1510,26 @@ var Schematic = class {
|
|
|
1386
1510
|
check
|
|
1387
1511
|
);
|
|
1388
1512
|
}
|
|
1513
|
+
if (typeof check.featureUsageEvent === "string") {
|
|
1514
|
+
this.updateFeatureUsageEventMap(check);
|
|
1515
|
+
}
|
|
1389
1516
|
listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
|
|
1390
1517
|
};
|
|
1518
|
+
/** Add or update a CheckFlagReturn in the featureUsageEventMap */
|
|
1519
|
+
updateFeatureUsageEventMap = (check) => {
|
|
1520
|
+
if (typeof check.featureUsageEvent !== "string") return;
|
|
1521
|
+
const eventName = check.featureUsageEvent;
|
|
1522
|
+
if (this.featureUsageEventMap[eventName] === void 0 || this.featureUsageEventMap[eventName] === null) {
|
|
1523
|
+
this.featureUsageEventMap[eventName] = {};
|
|
1524
|
+
}
|
|
1525
|
+
if (this.featureUsageEventMap[eventName] !== void 0) {
|
|
1526
|
+
this.featureUsageEventMap[eventName][check.flag] = check;
|
|
1527
|
+
}
|
|
1528
|
+
this.debug(
|
|
1529
|
+
`Updated featureUsageEventMap for event: ${eventName}, flag: ${check.flag}`,
|
|
1530
|
+
check
|
|
1531
|
+
);
|
|
1532
|
+
};
|
|
1391
1533
|
notifyFlagValueListeners = (flagKey, value) => {
|
|
1392
1534
|
const listeners = this.flagValueListeners?.[flagKey] ?? [];
|
|
1393
1535
|
if (listeners.size > 0) {
|
package/dist/schematic.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ declare interface CheckFlagResponse {
|
|
|
32
32
|
* Do not edit the class manually.
|
|
33
33
|
*/
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
*
|
|
36
36
|
* @export
|
|
37
37
|
* @interface CheckFlagResponseData
|
|
38
38
|
*/
|
|
@@ -61,6 +61,12 @@ export declare interface CheckFlagResponseData {
|
|
|
61
61
|
* @memberof CheckFlagResponseData
|
|
62
62
|
*/
|
|
63
63
|
featureUsage?: number | null;
|
|
64
|
+
/**
|
|
65
|
+
* If an event-based numeric feature entitlement rule was matched, the event used to track its usage
|
|
66
|
+
* @type {string}
|
|
67
|
+
* @memberof CheckFlagResponseData
|
|
68
|
+
*/
|
|
69
|
+
featureUsageEvent?: string | null;
|
|
64
70
|
/**
|
|
65
71
|
* For event-based feature entitlement rules, the period over which usage is tracked (current_month, current_day, current_week, all_time)
|
|
66
72
|
* @type {string}
|
|
@@ -130,6 +136,8 @@ export declare type CheckFlagReturn = {
|
|
|
130
136
|
featureAllocation?: number;
|
|
131
137
|
/** If a numeric feature entitlement rule was matched, the company's usage */
|
|
132
138
|
featureUsage?: number;
|
|
139
|
+
/** Event representing the feature usage */
|
|
140
|
+
featureUsageEvent?: string;
|
|
133
141
|
/** For event-based feature entitlement rules, the period over which usage is tracked (current_month, current_day, current_week, all_time) */
|
|
134
142
|
featureUsagePeriod?: UsagePeriod;
|
|
135
143
|
/** For event-based feature entitlement rules, when the usage period will reset */
|
|
@@ -175,7 +183,7 @@ declare interface CheckFlagsResponse {
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
/**
|
|
178
|
-
*
|
|
186
|
+
*
|
|
179
187
|
* @export
|
|
180
188
|
* @interface CheckFlagsResponseData
|
|
181
189
|
*/
|
|
@@ -349,6 +357,7 @@ export declare class Schematic {
|
|
|
349
357
|
private debugEnabled;
|
|
350
358
|
private offlineEnabled;
|
|
351
359
|
private eventQueue;
|
|
360
|
+
private contextDependentEventQueue;
|
|
352
361
|
private eventUrl;
|
|
353
362
|
private flagCheckListeners;
|
|
354
363
|
private flagValueListeners;
|
|
@@ -357,6 +366,7 @@ export declare class Schematic {
|
|
|
357
366
|
private storage;
|
|
358
367
|
private useWebSocket;
|
|
359
368
|
private checks;
|
|
369
|
+
private featureUsageEventMap;
|
|
360
370
|
private webSocketUrl;
|
|
361
371
|
constructor(apiKey: string, options?: SchematicOptions);
|
|
362
372
|
/**
|
|
@@ -410,11 +420,20 @@ export declare class Schematic {
|
|
|
410
420
|
/**
|
|
411
421
|
* Send a track event
|
|
412
422
|
* Track usage for a company and/or user.
|
|
423
|
+
* Optimistically updates feature usage flags if tracking a featureUsageEvent.
|
|
413
424
|
*/
|
|
414
425
|
track: (body: EventBodyTrack) => Promise<void>;
|
|
426
|
+
/**
|
|
427
|
+
* Optimistically update feature usage flags associated with a tracked event
|
|
428
|
+
* This updates flags in memory with updated usage counts and value/featureUsageExceeded flags
|
|
429
|
+
* before the network request completes
|
|
430
|
+
*/
|
|
431
|
+
private optimisticallyUpdateFeatureUsage;
|
|
415
432
|
/**
|
|
416
433
|
* Event processing
|
|
417
434
|
*/
|
|
435
|
+
private hasContext;
|
|
436
|
+
private flushContextDependentEventQueue;
|
|
418
437
|
private flushEventQueue;
|
|
419
438
|
private getAnonymousId;
|
|
420
439
|
private handleEvent;
|
|
@@ -436,13 +455,15 @@ export declare class Schematic {
|
|
|
436
455
|
getIsPending: () => boolean;
|
|
437
456
|
addIsPendingListener: (listener: PendingListenerFn) => () => void;
|
|
438
457
|
private setIsPending;
|
|
439
|
-
getFlagCheck: (flagKey: string) => CheckFlagReturn;
|
|
440
|
-
getFlagValue: (flagKey: string) => boolean;
|
|
458
|
+
getFlagCheck: (flagKey: string) => CheckFlagReturn | undefined;
|
|
459
|
+
getFlagValue: (flagKey: string) => boolean | undefined;
|
|
441
460
|
/** Register an event listener that will be notified with the boolean value for a given flag when this value changes */
|
|
442
461
|
addFlagValueListener: (flagKey: string, listener: FlagValueListenerFn) => () => void;
|
|
443
462
|
/** Register an event listener that will be notified with the full flag check response for a given flag whenever this value changes */
|
|
444
463
|
addFlagCheckListener: (flagKey: string, listener: FlagCheckListenerFn) => () => void;
|
|
445
464
|
private notifyFlagCheckListeners;
|
|
465
|
+
/** Add or update a CheckFlagReturn in the featureUsageEventMap */
|
|
466
|
+
private updateFeatureUsageEventMap;
|
|
446
467
|
private notifyFlagValueListeners;
|
|
447
468
|
}
|
|
448
469
|
|
package/dist/schematic.esm.js
CHANGED
|
@@ -28,20 +28,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var require_browser_polyfill = __commonJS({
|
|
29
29
|
"node_modules/cross-fetch/dist/browser-polyfill.js"(exports) {
|
|
30
30
|
(function(self2) {
|
|
31
|
-
var irrelevant = function(exports2) {
|
|
31
|
+
var irrelevant = (function(exports2) {
|
|
32
32
|
var g = typeof globalThis !== "undefined" && globalThis || typeof self2 !== "undefined" && self2 || // eslint-disable-next-line no-undef
|
|
33
33
|
typeof global !== "undefined" && global || {};
|
|
34
34
|
var support = {
|
|
35
35
|
searchParams: "URLSearchParams" in g,
|
|
36
36
|
iterable: "Symbol" in g && "iterator" in Symbol,
|
|
37
|
-
blob: "FileReader" in g && "Blob" in g && function() {
|
|
37
|
+
blob: "FileReader" in g && "Blob" in g && (function() {
|
|
38
38
|
try {
|
|
39
39
|
new Blob();
|
|
40
40
|
return true;
|
|
41
41
|
} catch (e) {
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
|
-
}(),
|
|
44
|
+
})(),
|
|
45
45
|
formData: "FormData" in g,
|
|
46
46
|
arrayBuffer: "ArrayBuffer" in g
|
|
47
47
|
};
|
|
@@ -343,12 +343,12 @@ var require_browser_polyfill = __commonJS({
|
|
|
343
343
|
}
|
|
344
344
|
this.method = normalizeMethod(options.method || this.method || "GET");
|
|
345
345
|
this.mode = options.mode || this.mode || null;
|
|
346
|
-
this.signal = options.signal || this.signal || function() {
|
|
346
|
+
this.signal = options.signal || this.signal || (function() {
|
|
347
347
|
if ("AbortController" in g) {
|
|
348
348
|
var ctrl = new AbortController();
|
|
349
349
|
return ctrl.signal;
|
|
350
350
|
}
|
|
351
|
-
}();
|
|
351
|
+
})();
|
|
352
352
|
this.referrer = null;
|
|
353
353
|
if ((this.method === "GET" || this.method === "HEAD") && body) {
|
|
354
354
|
throw new TypeError("Body not allowed for GET or HEAD requests");
|
|
@@ -555,12 +555,12 @@ var require_browser_polyfill = __commonJS({
|
|
|
555
555
|
exports2.Response = Response;
|
|
556
556
|
exports2.fetch = fetch2;
|
|
557
557
|
return exports2;
|
|
558
|
-
}({});
|
|
558
|
+
})({});
|
|
559
559
|
})(typeof self !== "undefined" ? self : exports);
|
|
560
560
|
}
|
|
561
561
|
});
|
|
562
562
|
|
|
563
|
-
// node_modules/uuid/dist/
|
|
563
|
+
// node_modules/uuid/dist/stringify.js
|
|
564
564
|
var byteToHex = [];
|
|
565
565
|
for (let i = 0; i < 256; ++i) {
|
|
566
566
|
byteToHex.push((i + 256).toString(16).slice(1));
|
|
@@ -569,7 +569,7 @@ function unsafeStringify(arr, offset = 0) {
|
|
|
569
569
|
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
-
// node_modules/uuid/dist/
|
|
572
|
+
// node_modules/uuid/dist/rng.js
|
|
573
573
|
var getRandomValues;
|
|
574
574
|
var rnds8 = new Uint8Array(16);
|
|
575
575
|
function rng() {
|
|
@@ -582,15 +582,12 @@ function rng() {
|
|
|
582
582
|
return getRandomValues(rnds8);
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
-
// node_modules/uuid/dist/
|
|
585
|
+
// node_modules/uuid/dist/native.js
|
|
586
586
|
var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
|
|
587
587
|
var native_default = { randomUUID };
|
|
588
588
|
|
|
589
|
-
// node_modules/uuid/dist/
|
|
590
|
-
function
|
|
591
|
-
if (native_default.randomUUID && !buf && !options) {
|
|
592
|
-
return native_default.randomUUID();
|
|
593
|
-
}
|
|
589
|
+
// node_modules/uuid/dist/v4.js
|
|
590
|
+
function _v4(options, buf, offset) {
|
|
594
591
|
options = options || {};
|
|
595
592
|
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
596
593
|
if (rnds.length < 16) {
|
|
@@ -610,6 +607,12 @@ function v4(options, buf, offset) {
|
|
|
610
607
|
}
|
|
611
608
|
return unsafeStringify(rnds);
|
|
612
609
|
}
|
|
610
|
+
function v4(options, buf, offset) {
|
|
611
|
+
if (native_default.randomUUID && !buf && !options) {
|
|
612
|
+
return native_default.randomUUID();
|
|
613
|
+
}
|
|
614
|
+
return _v4(options, buf, offset);
|
|
615
|
+
}
|
|
613
616
|
var v4_default = v4;
|
|
614
617
|
|
|
615
618
|
// src/index.ts
|
|
@@ -628,6 +631,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
628
631
|
error: json["error"] == null ? void 0 : json["error"],
|
|
629
632
|
featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
|
|
630
633
|
featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
|
|
634
|
+
featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
|
|
631
635
|
featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
|
|
632
636
|
featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
|
|
633
637
|
flag: json["flag"],
|
|
@@ -727,6 +731,7 @@ var CheckFlagReturnFromJSON = (json) => {
|
|
|
727
731
|
error,
|
|
728
732
|
featureAllocation,
|
|
729
733
|
featureUsage,
|
|
734
|
+
featureUsageEvent,
|
|
730
735
|
featureUsagePeriod,
|
|
731
736
|
featureUsageResetAt,
|
|
732
737
|
flag,
|
|
@@ -746,6 +751,7 @@ var CheckFlagReturnFromJSON = (json) => {
|
|
|
746
751
|
error: error == null ? void 0 : error,
|
|
747
752
|
featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
|
|
748
753
|
featureUsage: featureUsage == null ? void 0 : featureUsage,
|
|
754
|
+
featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
|
|
749
755
|
featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
|
|
750
756
|
featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
|
|
751
757
|
flag,
|
|
@@ -775,7 +781,7 @@ function contextString(context) {
|
|
|
775
781
|
}
|
|
776
782
|
|
|
777
783
|
// src/version.ts
|
|
778
|
-
var version = "1.2.
|
|
784
|
+
var version = "1.2.5";
|
|
779
785
|
|
|
780
786
|
// src/index.ts
|
|
781
787
|
var anonymousIdKey = "schematicId";
|
|
@@ -788,6 +794,7 @@ var Schematic = class {
|
|
|
788
794
|
debugEnabled = false;
|
|
789
795
|
offlineEnabled = false;
|
|
790
796
|
eventQueue;
|
|
797
|
+
contextDependentEventQueue;
|
|
791
798
|
eventUrl = "https://c.schematichq.com";
|
|
792
799
|
flagCheckListeners = {};
|
|
793
800
|
flagValueListeners = {};
|
|
@@ -796,10 +803,12 @@ var Schematic = class {
|
|
|
796
803
|
storage;
|
|
797
804
|
useWebSocket = false;
|
|
798
805
|
checks = {};
|
|
806
|
+
featureUsageEventMap = {};
|
|
799
807
|
webSocketUrl = "wss://api.schematichq.com";
|
|
800
808
|
constructor(apiKey, options) {
|
|
801
809
|
this.apiKey = apiKey;
|
|
802
810
|
this.eventQueue = [];
|
|
811
|
+
this.contextDependentEventQueue = [];
|
|
803
812
|
this.useWebSocket = options?.useWebSocket ?? false;
|
|
804
813
|
this.debugEnabled = options?.debug ?? false;
|
|
805
814
|
this.offlineEnabled = options?.offline ?? false;
|
|
@@ -842,6 +851,7 @@ var Schematic = class {
|
|
|
842
851
|
if (typeof window !== "undefined" && window?.addEventListener) {
|
|
843
852
|
window.addEventListener("beforeunload", () => {
|
|
844
853
|
this.flushEventQueue();
|
|
854
|
+
this.flushContextDependentEventQueue();
|
|
845
855
|
});
|
|
846
856
|
}
|
|
847
857
|
if (this.offlineEnabled) {
|
|
@@ -890,6 +900,9 @@ var Schematic = class {
|
|
|
890
900
|
const parsedResponse = CheckFlagResponseFromJSON(response);
|
|
891
901
|
this.debug(`checkFlag result: ${key}`, parsedResponse);
|
|
892
902
|
const result = CheckFlagReturnFromJSON(parsedResponse.data);
|
|
903
|
+
if (typeof result.featureUsageEvent === "string") {
|
|
904
|
+
this.updateFeatureUsageEventMap(result);
|
|
905
|
+
}
|
|
893
906
|
this.submitFlagCheckEvent(key, result, context);
|
|
894
907
|
return result.value;
|
|
895
908
|
}).catch((error) => {
|
|
@@ -1009,6 +1022,9 @@ var Schematic = class {
|
|
|
1009
1022
|
const data = CheckFlagResponseFromJSON(responseJson);
|
|
1010
1023
|
this.debug(`fallbackToRest result: ${key}`, data);
|
|
1011
1024
|
const result = CheckFlagReturnFromJSON(data.data);
|
|
1025
|
+
if (typeof result.featureUsageEvent === "string") {
|
|
1026
|
+
this.updateFeatureUsageEventMap(result);
|
|
1027
|
+
}
|
|
1012
1028
|
this.submitFlagCheckEvent(key, result, context);
|
|
1013
1029
|
return result.value;
|
|
1014
1030
|
} catch (error) {
|
|
@@ -1092,14 +1108,12 @@ var Schematic = class {
|
|
|
1092
1108
|
* In offline mode, this will just set the context locally without connecting.
|
|
1093
1109
|
*/
|
|
1094
1110
|
setContext = async (context) => {
|
|
1095
|
-
if (this.isOffline()) {
|
|
1111
|
+
if (this.isOffline() || !this.useWebSocket) {
|
|
1096
1112
|
this.context = context;
|
|
1113
|
+
this.flushContextDependentEventQueue();
|
|
1097
1114
|
this.setIsPending(false);
|
|
1098
1115
|
return Promise.resolve();
|
|
1099
1116
|
}
|
|
1100
|
-
if (!this.useWebSocket) {
|
|
1101
|
-
return Promise.resolve();
|
|
1102
|
-
}
|
|
1103
1117
|
try {
|
|
1104
1118
|
this.setIsPending(true);
|
|
1105
1119
|
if (!this.conn) {
|
|
@@ -1115,21 +1129,123 @@ var Schematic = class {
|
|
|
1115
1129
|
/**
|
|
1116
1130
|
* Send a track event
|
|
1117
1131
|
* Track usage for a company and/or user.
|
|
1132
|
+
* Optimistically updates feature usage flags if tracking a featureUsageEvent.
|
|
1118
1133
|
*/
|
|
1119
1134
|
track = (body) => {
|
|
1120
|
-
const { company, user, event, traits } = body;
|
|
1135
|
+
const { company, user, event, traits, quantity = 1 } = body;
|
|
1136
|
+
if (!this.hasContext(company, user)) {
|
|
1137
|
+
this.debug(`track: queuing event "${event}" until context is available`);
|
|
1138
|
+
const queuedEvent = {
|
|
1139
|
+
api_key: this.apiKey,
|
|
1140
|
+
body: {
|
|
1141
|
+
company,
|
|
1142
|
+
event,
|
|
1143
|
+
traits: traits ?? {},
|
|
1144
|
+
user,
|
|
1145
|
+
quantity
|
|
1146
|
+
},
|
|
1147
|
+
sent_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1148
|
+
tracker_event_id: v4_default(),
|
|
1149
|
+
tracker_user_id: this.getAnonymousId(),
|
|
1150
|
+
type: "track"
|
|
1151
|
+
};
|
|
1152
|
+
this.contextDependentEventQueue.push(queuedEvent);
|
|
1153
|
+
return Promise.resolve();
|
|
1154
|
+
}
|
|
1121
1155
|
const trackData = {
|
|
1122
1156
|
company: company ?? this.context.company,
|
|
1123
1157
|
event,
|
|
1124
1158
|
traits: traits ?? {},
|
|
1125
|
-
user: user ?? this.context.user
|
|
1159
|
+
user: user ?? this.context.user,
|
|
1160
|
+
quantity
|
|
1126
1161
|
};
|
|
1127
1162
|
this.debug(`track:`, trackData);
|
|
1163
|
+
if (event in this.featureUsageEventMap) {
|
|
1164
|
+
this.optimisticallyUpdateFeatureUsage(event, quantity);
|
|
1165
|
+
}
|
|
1128
1166
|
return this.handleEvent("track", trackData);
|
|
1129
1167
|
};
|
|
1168
|
+
/**
|
|
1169
|
+
* Optimistically update feature usage flags associated with a tracked event
|
|
1170
|
+
* This updates flags in memory with updated usage counts and value/featureUsageExceeded flags
|
|
1171
|
+
* before the network request completes
|
|
1172
|
+
*/
|
|
1173
|
+
optimisticallyUpdateFeatureUsage = (eventName, quantity = 1) => {
|
|
1174
|
+
const flagsForEvent = this.featureUsageEventMap[eventName];
|
|
1175
|
+
if (flagsForEvent === void 0 || flagsForEvent === null) return;
|
|
1176
|
+
this.debug(
|
|
1177
|
+
`Optimistically updating feature usage for event: ${eventName}`,
|
|
1178
|
+
{ quantity }
|
|
1179
|
+
);
|
|
1180
|
+
Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
|
|
1181
|
+
if (check === void 0) return;
|
|
1182
|
+
const updatedCheck = { ...check };
|
|
1183
|
+
if (typeof updatedCheck.featureUsage === "number") {
|
|
1184
|
+
updatedCheck.featureUsage += quantity;
|
|
1185
|
+
if (typeof updatedCheck.featureAllocation === "number") {
|
|
1186
|
+
const wasExceeded = updatedCheck.featureUsageExceeded === true;
|
|
1187
|
+
const nowExceeded = updatedCheck.featureUsage >= updatedCheck.featureAllocation;
|
|
1188
|
+
if (nowExceeded !== wasExceeded) {
|
|
1189
|
+
updatedCheck.featureUsageExceeded = nowExceeded;
|
|
1190
|
+
if (nowExceeded) {
|
|
1191
|
+
updatedCheck.value = false;
|
|
1192
|
+
}
|
|
1193
|
+
this.debug(`Usage limit status changed for flag: ${flagKey}`, {
|
|
1194
|
+
was: wasExceeded ? "exceeded" : "within limits",
|
|
1195
|
+
now: nowExceeded ? "exceeded" : "within limits",
|
|
1196
|
+
featureUsage: updatedCheck.featureUsage,
|
|
1197
|
+
featureAllocation: updatedCheck.featureAllocation,
|
|
1198
|
+
value: updatedCheck.value
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (this.featureUsageEventMap[eventName] !== void 0) {
|
|
1203
|
+
this.featureUsageEventMap[eventName][flagKey] = updatedCheck;
|
|
1204
|
+
}
|
|
1205
|
+
const contextStr = contextString(this.context);
|
|
1206
|
+
if (this.checks[contextStr] !== void 0 && this.checks[contextStr] !== null) {
|
|
1207
|
+
this.checks[contextStr][flagKey] = updatedCheck;
|
|
1208
|
+
}
|
|
1209
|
+
this.notifyFlagCheckListeners(flagKey, updatedCheck);
|
|
1210
|
+
this.notifyFlagValueListeners(flagKey, updatedCheck.value);
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
};
|
|
1130
1214
|
/**
|
|
1131
1215
|
* Event processing
|
|
1132
1216
|
*/
|
|
1217
|
+
hasContext = (company, user) => {
|
|
1218
|
+
const hasProvidedContext = company !== void 0 && company !== null && Object.keys(company).length > 0 || user !== void 0 && user !== null && Object.keys(user).length > 0;
|
|
1219
|
+
const hasInstanceContext = this.context.company !== void 0 && this.context.company !== null && Object.keys(this.context.company).length > 0 || this.context.user !== void 0 && this.context.user !== null && Object.keys(this.context.user).length > 0;
|
|
1220
|
+
return hasProvidedContext || hasInstanceContext;
|
|
1221
|
+
};
|
|
1222
|
+
flushContextDependentEventQueue = () => {
|
|
1223
|
+
this.debug(
|
|
1224
|
+
`flushing ${this.contextDependentEventQueue.length} context-dependent events`
|
|
1225
|
+
);
|
|
1226
|
+
while (this.contextDependentEventQueue.length > 0) {
|
|
1227
|
+
const event = this.contextDependentEventQueue.shift();
|
|
1228
|
+
if (event) {
|
|
1229
|
+
if (event.type === "track" && typeof event.body === "object" && event.body !== null) {
|
|
1230
|
+
const trackBody = event.body;
|
|
1231
|
+
const updatedBody = {
|
|
1232
|
+
...trackBody,
|
|
1233
|
+
company: trackBody.company ?? this.context.company,
|
|
1234
|
+
user: trackBody.user ?? this.context.user
|
|
1235
|
+
};
|
|
1236
|
+
const updatedEvent = {
|
|
1237
|
+
...event,
|
|
1238
|
+
body: updatedBody,
|
|
1239
|
+
sent_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1240
|
+
// Update timestamp to actual send time
|
|
1241
|
+
};
|
|
1242
|
+
this.sendEvent(updatedEvent);
|
|
1243
|
+
} else {
|
|
1244
|
+
this.sendEvent(event);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1133
1249
|
flushEventQueue = () => {
|
|
1134
1250
|
while (this.eventQueue.length > 0) {
|
|
1135
1251
|
const event = this.eventQueue.shift();
|
|
@@ -1269,18 +1385,26 @@ var Schematic = class {
|
|
|
1269
1385
|
}
|
|
1270
1386
|
(message.flags ?? []).forEach((flag) => {
|
|
1271
1387
|
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1272
|
-
|
|
1388
|
+
const contextStr = contextString(context);
|
|
1389
|
+
if (this.checks[contextStr] === void 0) {
|
|
1390
|
+
this.checks[contextStr] = {};
|
|
1391
|
+
}
|
|
1392
|
+
this.checks[contextStr][flagCheck.flag] = flagCheck;
|
|
1273
1393
|
this.debug(`WebSocket flag update:`, {
|
|
1274
1394
|
flag: flagCheck.flag,
|
|
1275
1395
|
value: flagCheck.value,
|
|
1276
1396
|
flagCheck
|
|
1277
1397
|
});
|
|
1398
|
+
if (typeof flagCheck.featureUsageEvent === "string") {
|
|
1399
|
+
this.updateFeatureUsageEventMap(flagCheck);
|
|
1400
|
+
}
|
|
1278
1401
|
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1279
1402
|
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1280
1403
|
}
|
|
1281
1404
|
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1282
1405
|
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1283
1406
|
});
|
|
1407
|
+
this.flushContextDependentEventQueue();
|
|
1284
1408
|
this.setIsPending(false);
|
|
1285
1409
|
if (!resolved) {
|
|
1286
1410
|
resolved = true;
|
|
@@ -1367,8 +1491,26 @@ var Schematic = class {
|
|
|
1367
1491
|
check
|
|
1368
1492
|
);
|
|
1369
1493
|
}
|
|
1494
|
+
if (typeof check.featureUsageEvent === "string") {
|
|
1495
|
+
this.updateFeatureUsageEventMap(check);
|
|
1496
|
+
}
|
|
1370
1497
|
listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
|
|
1371
1498
|
};
|
|
1499
|
+
/** Add or update a CheckFlagReturn in the featureUsageEventMap */
|
|
1500
|
+
updateFeatureUsageEventMap = (check) => {
|
|
1501
|
+
if (typeof check.featureUsageEvent !== "string") return;
|
|
1502
|
+
const eventName = check.featureUsageEvent;
|
|
1503
|
+
if (this.featureUsageEventMap[eventName] === void 0 || this.featureUsageEventMap[eventName] === null) {
|
|
1504
|
+
this.featureUsageEventMap[eventName] = {};
|
|
1505
|
+
}
|
|
1506
|
+
if (this.featureUsageEventMap[eventName] !== void 0) {
|
|
1507
|
+
this.featureUsageEventMap[eventName][check.flag] = check;
|
|
1508
|
+
}
|
|
1509
|
+
this.debug(
|
|
1510
|
+
`Updated featureUsageEventMap for event: ${eventName}, flag: ${check.flag}`,
|
|
1511
|
+
check
|
|
1512
|
+
);
|
|
1513
|
+
};
|
|
1372
1514
|
notifyFlagValueListeners = (flagKey, value) => {
|
|
1373
1515
|
const listeners = this.flagValueListeners?.[flagKey] ?? [];
|
|
1374
1516
|
if (listeners.size > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schematichq/schematic-js",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"main": "dist/schematic.cjs.js",
|
|
5
5
|
"module": "dist/schematic.esm.js",
|
|
6
6
|
"types": "dist/schematic.d.ts",
|
|
@@ -26,30 +26,34 @@
|
|
|
26
26
|
"format": "prettier --write src/*.ts",
|
|
27
27
|
"lint": "eslint src --report-unused-disable-directives --fix",
|
|
28
28
|
"openapi": "rm -rf src/types/api/ && npx openapi-generator-cli generate -c openapi-config.yaml --global-property models=\"EventBody:EventBodyFlagCheck:EventBodyIdentify:EventBodyIdentifyCompany:EventBodyTrack:CheckFlagResponse:CheckFlagResponseData:CheckFlagsResponse:CheckFlagsResponseData\",supportingFiles=runtime.ts && prettier --write \"src/types/api/**/*.{ts,tsx}\"",
|
|
29
|
-
"
|
|
29
|
+
"prepare": "husky",
|
|
30
|
+
"test": "jest --config jest.config.js",
|
|
31
|
+
"tsc": "npx tsc"
|
|
30
32
|
},
|
|
31
33
|
"dependencies": {
|
|
32
34
|
"cross-fetch": "^4.1.0",
|
|
33
|
-
"uuid": "^
|
|
35
|
+
"uuid": "^13.0.0"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
38
|
-
"@
|
|
38
|
+
"@eslint/js": "^9.24.0",
|
|
39
|
+
"@microsoft/api-extractor": "^7.52.2",
|
|
40
|
+
"@openapitools/openapi-generator-cli": "^2.18.4",
|
|
41
|
+
"@types/jest": "^30.0.0",
|
|
39
42
|
"@types/uuid": "^10.0.0",
|
|
40
|
-
"
|
|
41
|
-
"@typescript-eslint/parser": "^8.23.0",
|
|
42
|
-
"esbuild": "^0.25.0",
|
|
43
|
+
"esbuild": "^0.25.2",
|
|
43
44
|
"esbuild-jest": "^0.5.0",
|
|
44
|
-
"eslint": "^
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"jest
|
|
45
|
+
"eslint": "^9.24.0",
|
|
46
|
+
"globals": "^16.0.0",
|
|
47
|
+
"husky": "^9.1.7",
|
|
48
|
+
"jest": "^30.0.0",
|
|
49
|
+
"jest-environment-jsdom": "^30.0.0",
|
|
50
|
+
"jest-esbuild": "^0.4.0",
|
|
48
51
|
"jest-fetch-mock": "^3.0.3",
|
|
49
52
|
"mock-socket": "^9.3.1",
|
|
50
|
-
"prettier": "^3.
|
|
51
|
-
"ts-jest": "^29.
|
|
52
|
-
"typescript": "^5.
|
|
53
|
+
"prettier": "^3.6.2",
|
|
54
|
+
"ts-jest": "^29.3.0",
|
|
55
|
+
"typescript": "^5.9.2",
|
|
56
|
+
"typescript-eslint": "^8.29.1"
|
|
53
57
|
},
|
|
54
58
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
55
59
|
}
|