@schematichq/schematic-js 1.2.0 → 1.2.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.
- package/README.md +30 -0
- package/dist/schematic.browser.js +2 -2
- package/dist/schematic.cjs.js +238 -18
- package/dist/schematic.d.ts +113 -2
- package/dist/schematic.esm.js +238 -18
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -82,6 +82,36 @@ await schematic.checkFlag("some-flag-key");
|
|
|
82
82
|
schematic.cleanup();
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
## Troubleshooting
|
|
86
|
+
|
|
87
|
+
For debugging and development, Schematic supports two special modes:
|
|
88
|
+
|
|
89
|
+
### Debug Mode
|
|
90
|
+
|
|
91
|
+
Enables console logging of all Schematic operations:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// Enable at initialization
|
|
95
|
+
const schematic = new Schematic("your-api-key", { debug: true });
|
|
96
|
+
|
|
97
|
+
// Or via URL parameter
|
|
98
|
+
// https://yoursite.com/?schematic_debug=true
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Offline Mode
|
|
102
|
+
|
|
103
|
+
Prevents network requests and returns fallback values for all flag checks:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Enable at initialization
|
|
107
|
+
const schematic = new Schematic("your-api-key", { offline: true });
|
|
108
|
+
|
|
109
|
+
// Or via URL parameter
|
|
110
|
+
// https://yoursite.com/?schematic_offline=true
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Offline mode automatically enables debug mode to help with troubleshooting.
|
|
114
|
+
|
|
85
115
|
## License
|
|
86
116
|
|
|
87
117
|
MIT
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";(()=>{var
|
|
2
|
-
`)===0?u.substr(1,u.length):u}).forEach(function(u){var f=u.split(":"),l=f.shift().trim();if(l){var T=f.join(":").trim();try{a.append(l,T)}catch(A){console.warn("Response "+A.message)}}}),a}J.call(F.prototype);function b(e,a){if(!(this instanceof b))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 c(a.headers),this.url=a.url||"",this._initBody(e)}J.call(b.prototype),b.prototype.clone=function(){return new b(this._bodyInit,{status:this.status,statusText:this.statusText,headers:new c(this.headers),url:this.url})},b.error=function(){var e=new b(null,{status:200,statusText:""});return e.ok=!1,e.status=0,e.type="error",e};var ee=[301,302,303,307,308];b.redirect=function(e,a){if(ee.indexOf(a)===-1)throw new RangeError("Invalid status code");return new b(null,{status:a,headers:{location:e}})},n.DOMException=o.DOMException;try{new n.DOMException}catch{n.DOMException=function(a,i){this.message=a,this.name=i;var u=Error(a);this.stack=u.stack},n.DOMException.prototype=Object.create(Error.prototype),n.DOMException.prototype.constructor=n.DOMException}function O(e,a){return new Promise(function(i,u){var f=new F(e,a);if(f.signal&&f.signal.aborted)return u(new n.DOMException("Aborted","AbortError"));var l=new XMLHttpRequest;function T(){l.abort()}l.onload=function(){var y={statusText:l.statusText,headers:Z(l.getAllResponseHeaders()||"")};f.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 C="response"in l?l.response:l.responseText;setTimeout(function(){i(new b(C,y))},0)},l.onerror=function(){setTimeout(function(){u(new TypeError("Network request failed"))},0)},l.ontimeout=function(){setTimeout(function(){u(new TypeError("Network request timed out"))},0)},l.onabort=function(){setTimeout(function(){u(new n.DOMException("Aborted","AbortError"))},0)};function A(y){try{return y===""&&o.location.href?o.location.href:y}catch{return y}}if(l.open(f.method,A(f.url),!0),f.credentials==="include"?l.withCredentials=!0:f.credentials==="omit"&&(l.withCredentials=!1),"responseType"in l&&(s.blob?l.responseType="blob":s.arrayBuffer&&(l.responseType="arraybuffer")),a&&typeof a.headers=="object"&&!(a.headers instanceof c||o.Headers&&a.headers instanceof o.Headers)){var V=[];Object.getOwnPropertyNames(a.headers).forEach(function(y){V.push(m(y)),l.setRequestHeader(y,E(a.headers[y]))}),f.headers.forEach(function(y,C){V.indexOf(C)===-1&&l.setRequestHeader(C,y)})}else f.headers.forEach(function(y,C){l.setRequestHeader(C,y)});f.signal&&(f.signal.addEventListener("abort",T),l.onreadystatechange=function(){l.readyState===4&&f.signal.removeEventListener("abort",T)}),l.send(typeof f._bodyInit>"u"?null:f._bodyInit)})}return O.polyfill=!0,o.fetch||(o.fetch=O,o.Headers=c,o.Request=F,o.Response=b),n.Headers=c,n.Request=F,n.Response=b,n.fetch=O,n}({})})(typeof self<"u"?self:q)});var g=[];for(let r=0;r<256;++r)g.push((r+256).toString(16).slice(1));function M(r,t=0){return(g[r[t+0]]+g[r[t+1]]+g[r[t+2]]+g[r[t+3]]+"-"+g[r[t+4]]+g[r[t+5]]+"-"+g[r[t+6]]+g[r[t+7]]+"-"+g[r[t+8]]+g[r[t+9]]+"-"+g[r[t+10]]+g[r[t+11]]+g[r[t+12]]+g[r[t+13]]+g[r[t+14]]+g[r[t+15]]).toLowerCase()}var U,ce=new Uint8Array(16);function L(){if(!U){if(typeof crypto>"u"||!crypto.getRandomValues)throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");U=crypto.getRandomValues.bind(crypto)}return U(ce)}var ue=typeof crypto<"u"&&crypto.randomUUID&&crypto.randomUUID.bind(crypto),P={randomUUID:ue};function fe(r,t,n){if(P.randomUUID&&!t&&!r)return P.randomUUID();r=r||{};let o=r.random??r.rng?.()??L();if(o.length<16)throw new Error("Random bytes length must be >= 16");if(o[6]=o[6]&15|64,o[8]=o[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 s=0;s<16;++s)t[n+s]=o[s];return t}return M(o)}var w=fe;var ze=le(W());function S(r){return he(r,!1)}function he(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 _(r){return ye(r,!1)}function ye(r,t){return r==null?r:{data:S(r.data),params:r.params}}function K(r){return ge(r,!1)}function ge(r,t){return r==null?r:{flags:r.flags.map(S)}}function I(r){return me(r,!1)}function me(r,t){return r==null?r:{data:K(r.data),params:r.params}}var G=r=>{let{companyId:t,error:n,featureAllocation:o,featureUsage:s,featureUsagePeriod:h,featureUsageResetAt:d,flag:p,flagId:m,reason:E,ruleId:k,ruleType:c,userId:R,value:x}=S(r);return{featureUsageExceeded:!x&&(c=="company_override_usage_exceeded"||c=="plan_entitlement_usage_exceeded"),companyId:t??void 0,error:n??void 0,featureAllocation:o??void 0,featureUsage:s??void 0,featureUsagePeriod:h??void 0,featureUsageResetAt:d??void 0,flag:p,flagId:m??void 0,reason:E,ruleId:k??void 0,ruleType:c??void 0,userId:R??void 0,value:x}};function v(r){let t=Object.keys(r).reduce((n,o)=>{let h=Object.keys(r[o]||{}).sort().reduce((d,p)=>(d[p]=r[o][p],d),{});return n[o]=h,n},{});return JSON.stringify(t)}var X="schematicId";var D=class{additionalHeaders={};apiKey;apiUrl="https://api.schematichq.com";conn=null;context={};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){this.apiKey=t,this.eventQueue=[],this.useWebSocket=n?.useWebSocket??!1,n?.additionalHeaders&&(this.additionalHeaders=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()})}async checkFlag(t){let{fallback:n=!1,key:o}=t,s=t.context||this.context,h=v(s);if(!this.useWebSocket){let d=`${this.apiUrl}/flags/${o}/check`;return fetch(d,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(s)}).then(p=>{if(!p.ok)throw new Error("Network response was not ok");return p.json()}).then(p=>_(p).data.value).catch(p=>(console.error("There was a problem with the fetch operation:",p),n))}try{let d=this.checks[h];if(this.conn&&typeof d<"u"&&typeof d[o]<"u")return d[o].value;try{await this.setContext(s)}catch(m){return console.error("WebSocket connection failed, falling back to REST:",m),this.fallbackToRest(o,s,n)}return(this.checks[h]??{})[o]?.value??n}catch(d){return console.error("Unexpected error in checkFlag:",d),n}}async fallbackToRest(t,n,o){try{let s=`${this.apiUrl}/flags/${t}/check`,h=await fetch(s,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8","X-Schematic-Api-Key":this.apiKey},body:JSON.stringify(n)});if(!h.ok)throw new Error("Network response was not ok");return _(await h.json())?.data?.value??!1}catch(s){return console.error("REST API call failed, using fallback value:",s),o}}checkFlags=async t=>{t=t||this.context;let n=`${this.apiUrl}/flags/check`,o=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:o}).then(s=>{if(!s.ok)throw new Error("Network response was not ok");return s.json()}).then(s=>(I(s)?.data?.flags??[]).reduce((d,p)=>(d[p.flag]=p.value,d),{})).catch(s=>(console.error("There was a problem with the fetch operation:",s),{}))};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.useWebSocket)return this.context=t,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:o,event:s,traits:h}=t;return this.handleEvent("track",{company:n??this.context.company,event:s,traits:h??{},user:o??this.context.user})};flushEventQueue=()=>{for(;this.eventQueue.length>0;){let t=this.eventQueue.shift();t&&this.sendEvent(t)}};getAnonymousId=()=>{if(!this.storage)return w();let t=this.storage.getItem(X);if(typeof t<"u")return t;let n=w();return this.storage.setItem(X,n),n};handleEvent=(t,n)=>{let o={api_key:this.apiKey,body:n,sent_at:new Date().toISOString(),tracker_event_id:w(),tracker_user_id:this.getAnonymousId(),type:t};return document?.hidden?this.storeEvent(o):this.sendEvent(o)};sendEvent=async t=>{let n=`${this.eventUrl}/e`,o=JSON.stringify(t);try{await fetch(n,{method:"POST",headers:{...this.additionalHeaders??{},"Content-Type":"application/json;charset=UTF-8"},body:o})}catch(s){console.error("Error sending Schematic event: ",s)}return Promise.resolve()};storeEvent=t=>(this.eventQueue.push(t),Promise.resolve());cleanup=async()=>{if(this.conn)try{(await this.conn).close()}catch(t){console.error("Error during cleanup:",t)}finally{this.conn=null}};wsConnect=()=>new Promise((t,n)=>{let o=`${this.webSocketUrl}/flags/bootstrap`,s=new WebSocket(o);s.onopen=()=>{t(s)},s.onerror=h=>{n(h)},s.onclose=()=>{this.conn=null}});wsSendMessage=(t,n)=>new Promise((o,s)=>{if(v(n)==v(this.context))return o(this.setIsPending(!1));this.context=n;let h=()=>{let d=!1,p=m=>{let E=JSON.parse(m.data);v(n)in this.checks||(this.checks[v(n)]={}),(E.flags??[]).forEach(k=>{let c=G(k);this.checks[v(n)][c.flag]=c,this.notifyFlagCheckListeners(k.flag,c),this.notifyFlagValueListeners(k.flag,c.value)}),this.setIsPending(!1),d||(d=!0,o())};t.addEventListener("message",p),t.send(JSON.stringify({apiKey:this.apiKey,data:n}))};t.readyState===WebSocket.OPEN?h():t.readyState===WebSocket.CONNECTING?t.addEventListener("open",h):s("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=>be(n,t))};getFlagCheck=t=>{let n=v(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)=>{(this.flagCheckListeners?.[t]??[]).forEach(s=>ke(s,n))};notifyFlagValueListeners=(t,n)=>{(this.flagValueListeners?.[t]??[]).forEach(s=>Fe(s,n))}},be=(r,t)=>{r.length>0?r(t):r()},ke=(r,t)=>{r.length>0?r(t):r()},Fe=(r,t)=>{r.length>0?r(t):r()};window.Schematic=D;})();
|
|
1
|
+
"use strict";(()=>{var ne=Object.create;var M=Object.defineProperty;var se=Object.getOwnPropertyDescriptor;var ae=Object.getOwnPropertyNames;var ie=Object.getPrototypeOf,oe=Object.prototype.hasOwnProperty;var le=(r,t)=>()=>(t||r((t={exports:{}}).exports,t),t.exports);var ce=(r,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of ae(t))!oe.call(r,i)&&i!==n&&M(r,i,{get:()=>t[i],enumerable:!(s=se(t,i))||s.enumerable});return r};var ue=(r,t,n)=>(n=r!=null?ne(ie(r)):{},ce(t||!r||!r.__esModule?M(n,"default",{value:r,enumerable:!0}):n,r));var z=le(K=>{(function(r){var t=function(n){var s=typeof globalThis<"u"&&globalThis||typeof r<"u"&&r||typeof global<"u"&&global||{},i={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(e){return e&&DataView.prototype.isPrototypeOf(e)}if(i.arrayBuffer)var u=["[object Int8Array]","[object Uint8Array]","[object Uint8ClampedArray]","[object Int16Array]","[object Uint16Array]","[object Int32Array]","[object Uint32Array]","[object Float32Array]","[object Float64Array]"],f=ArrayBuffer.isView||function(e){return e&&u.indexOf(Object.prototype.toString.call(e))>-1};function p(e){if(typeof e!="string"&&(e=String(e)),/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(e)||e==="")throw new TypeError('Invalid character in header field name: "'+e+'"');return e.toLowerCase()}function k(e){return typeof e!="string"&&(e=String(e)),e}function E(e){var a={next:function(){var o=e.shift();return{done:o===void 0,value:o}}};return i.iterable&&(a[Symbol.iterator]=function(){return a}),a}function d(e){this.map={},e instanceof d?e.forEach(function(a,o){this.append(o,a)},this):Array.isArray(e)?e.forEach(function(a){if(a.length!=2)throw new TypeError("Headers constructor: expected name/value pair to be length 2, found"+a.length);this.append(a[0],a[1])},this):e&&Object.getOwnPropertyNames(e).forEach(function(a){this.append(a,e[a])},this)}d.prototype.append=function(e,a){e=p(e),a=k(a);var o=this.map[e];this.map[e]=o?o+", "+a:a},d.prototype.delete=function(e){delete this.map[p(e)]},d.prototype.get=function(e){return e=p(e),this.has(e)?this.map[e]:null},d.prototype.has=function(e){return this.map.hasOwnProperty(p(e))},d.prototype.set=function(e,a){this.map[p(e)]=k(a)},d.prototype.forEach=function(e,a){for(var o in this.map)this.map.hasOwnProperty(o)&&e.call(a,this.map[o],o,this)},d.prototype.keys=function(){var e=[];return this.forEach(function(a,o){e.push(o)}),E(e)},d.prototype.values=function(){var e=[];return this.forEach(function(a){e.push(a)}),E(e)},d.prototype.entries=function(){var e=[];return this.forEach(function(a,o){e.push([o,a])}),E(e)},i.iterable&&(d.prototype[Symbol.iterator]=d.prototype.entries);function F(e){if(!e._noBody){if(e.bodyUsed)return Promise.reject(new TypeError("Already read"));e.bodyUsed=!0}}function b(e){return new Promise(function(a,o){e.onload=function(){a(e.result)},e.onerror=function(){o(e.error)}})}function V(e){var a=new FileReader,o=b(a);return a.readAsArrayBuffer(e),o}function Q(e){var a=new FileReader,o=b(a),h=/charset=([A-Za-z0-9_-]+)/.exec(e.type),g=h?h[1]:"utf-8";return a.readAsText(e,g),o}function Y(e){for(var a=new Uint8Array(e),o=new Array(a.length),h=0;h<a.length;h++)o[h]=String.fromCharCode(a[h]);return o.join("")}function q(e){if(e.slice)return e.slice(0);var a=new Uint8Array(e.byteLength);return a.set(new Uint8Array(e)),a.buffer}function H(){return this.bodyUsed=!1,this._initBody=function(e){this.bodyUsed=this.bodyUsed,this._bodyInit=e,e?typeof e=="string"?this._bodyText=e:i.blob&&Blob.prototype.isPrototypeOf(e)?this._bodyBlob=e:i.formData&&FormData.prototype.isPrototypeOf(e)?this._bodyFormData=e:i.searchParams&&URLSearchParams.prototype.isPrototypeOf(e)?this._bodyText=e.toString():i.arrayBuffer&&i.blob&&c(e)?(this._bodyArrayBuffer=q(e.buffer),this._bodyInit=new Blob([this._bodyArrayBuffer])):i.arrayBuffer&&(ArrayBuffer.prototype.isPrototypeOf(e)||f(e))?this._bodyArrayBuffer=q(e):this._bodyText=e=Object.prototype.toString.call(e):(this._noBody=!0,this._bodyText=""),this.headers.get("content-type")||(typeof e=="string"?this.headers.set("content-type","text/plain;charset=UTF-8"):this._bodyBlob&&this._bodyBlob.type?this.headers.set("content-type",this._bodyBlob.type):i.searchParams&&URLSearchParams.prototype.isPrototypeOf(e)&&this.headers.set("content-type","application/x-www-form-urlencoded;charset=UTF-8"))},i.blob&&(this.blob=function(){var e=F(this);if(e)return e;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 e=F(this);return e||(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(i.blob)return this.blob().then(V);throw new Error("could not read as ArrayBuffer")}},this.text=function(){var e=F(this);if(e)return e;if(this._bodyBlob)return Q(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)},i.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(e){var a=e.toUpperCase();return Z.indexOf(a)>-1?a:e}function C(e,a){if(!(this instanceof C))throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.');a=a||{};var o=a.body;if(e instanceof C){if(e.bodyUsed)throw new TypeError("Already read");this.url=e.url,this.credentials=e.credentials,a.headers||(this.headers=new d(e.headers)),this.method=e.method,this.mode=e.mode,this.signal=e.signal,!o&&e._bodyInit!=null&&(o=e._bodyInit,e.bodyUsed=!0)}else this.url=String(e);if(this.credentials=a.credentials||this.credentials||"same-origin",(a.headers||!this.headers)&&(this.headers=new d(a.headers)),this.method=j(a.method||this.method||"GET"),this.mode=a.mode||this.mode||null,this.signal=a.signal||this.signal||function(){if("AbortController"in s){var l=new AbortController;return l.signal}}(),this.referrer=null,(this.method==="GET"||this.method==="HEAD")&&o)throw new TypeError("Body not allowed for GET or HEAD requests");if(this._initBody(o),(this.method==="GET"||this.method==="HEAD")&&(a.cache==="no-store"||a.cache==="no-cache")){var h=/([?&])_=[^&]*/;if(h.test(this.url))this.url=this.url.replace(h,"$1_="+new Date().getTime());else{var g=/\?/;this.url+=(g.test(this.url)?"&":"?")+"_="+new Date().getTime()}}}C.prototype.clone=function(){return new C(this,{body:this._bodyInit})};function ee(e){var a=new FormData;return e.trim().split("&").forEach(function(o){if(o){var h=o.split("="),g=h.shift().replace(/\+/g," "),l=h.join("=").replace(/\+/g," ");a.append(decodeURIComponent(g),decodeURIComponent(l))}}),a}function te(e){var a=new d,o=e.replace(/\r?\n[\t ]+/g," ");return o.split("\r").map(function(h){return h.indexOf(`
|
|
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;})();
|
|
3
3
|
/* @preserve */
|
package/dist/schematic.cjs.js
CHANGED
|
@@ -572,6 +572,7 @@ __export(index_exports, {
|
|
|
572
572
|
CheckFlagResponseFromJSON: () => CheckFlagResponseFromJSON,
|
|
573
573
|
CheckFlagReturnFromJSON: () => CheckFlagReturnFromJSON,
|
|
574
574
|
CheckFlagsResponseFromJSON: () => CheckFlagsResponseFromJSON,
|
|
575
|
+
EventBodyFlagCheckToJSON: () => EventBodyFlagCheckToJSON,
|
|
575
576
|
RuleType: () => RuleType,
|
|
576
577
|
Schematic: () => Schematic,
|
|
577
578
|
UsagePeriod: () => UsagePeriod
|
|
@@ -658,6 +659,28 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
658
659
|
};
|
|
659
660
|
}
|
|
660
661
|
|
|
662
|
+
// src/types/api/models/EventBodyFlagCheck.ts
|
|
663
|
+
function EventBodyFlagCheckToJSON(json) {
|
|
664
|
+
return EventBodyFlagCheckToJSONTyped(json, false);
|
|
665
|
+
}
|
|
666
|
+
function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
|
|
667
|
+
if (value == null) {
|
|
668
|
+
return value;
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
company_id: value["companyId"],
|
|
672
|
+
error: value["error"],
|
|
673
|
+
flag_id: value["flagId"],
|
|
674
|
+
flag_key: value["flagKey"],
|
|
675
|
+
reason: value["reason"],
|
|
676
|
+
req_company: value["reqCompany"],
|
|
677
|
+
req_user: value["reqUser"],
|
|
678
|
+
rule_id: value["ruleId"],
|
|
679
|
+
user_id: value["userId"],
|
|
680
|
+
value: value["value"]
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
661
684
|
// src/types/api/models/CheckFlagResponse.ts
|
|
662
685
|
function CheckFlagResponseFromJSON(json) {
|
|
663
686
|
return CheckFlagResponseFromJSONTyped(json, false);
|
|
@@ -770,6 +793,9 @@ function contextString(context) {
|
|
|
770
793
|
return JSON.stringify(sortedContext);
|
|
771
794
|
}
|
|
772
795
|
|
|
796
|
+
// src/version.ts
|
|
797
|
+
var version = "1.2.2";
|
|
798
|
+
|
|
773
799
|
// src/index.ts
|
|
774
800
|
var anonymousIdKey = "schematicId";
|
|
775
801
|
var Schematic = class {
|
|
@@ -778,6 +804,8 @@ var Schematic = class {
|
|
|
778
804
|
apiUrl = "https://api.schematichq.com";
|
|
779
805
|
conn = null;
|
|
780
806
|
context = {};
|
|
807
|
+
debugEnabled = false;
|
|
808
|
+
offlineEnabled = false;
|
|
781
809
|
eventQueue;
|
|
782
810
|
eventUrl = "https://c.schematichq.com";
|
|
783
811
|
flagCheckListeners = {};
|
|
@@ -792,9 +820,30 @@ var Schematic = class {
|
|
|
792
820
|
this.apiKey = apiKey;
|
|
793
821
|
this.eventQueue = [];
|
|
794
822
|
this.useWebSocket = options?.useWebSocket ?? false;
|
|
795
|
-
|
|
796
|
-
|
|
823
|
+
this.debugEnabled = options?.debug ?? false;
|
|
824
|
+
this.offlineEnabled = options?.offline ?? false;
|
|
825
|
+
if (typeof window !== "undefined" && typeof window.location !== "undefined") {
|
|
826
|
+
const params = new URLSearchParams(window.location.search);
|
|
827
|
+
const debugParam = params.get("schematic_debug");
|
|
828
|
+
if (debugParam !== null && (debugParam === "" || debugParam === "true" || debugParam === "1")) {
|
|
829
|
+
this.debugEnabled = true;
|
|
830
|
+
}
|
|
831
|
+
const offlineParam = params.get("schematic_offline");
|
|
832
|
+
if (offlineParam !== null && (offlineParam === "" || offlineParam === "true" || offlineParam === "1")) {
|
|
833
|
+
this.offlineEnabled = true;
|
|
834
|
+
this.debugEnabled = true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (this.offlineEnabled && options?.debug !== false) {
|
|
838
|
+
this.debugEnabled = true;
|
|
797
839
|
}
|
|
840
|
+
if (this.offlineEnabled) {
|
|
841
|
+
this.setIsPending(false);
|
|
842
|
+
}
|
|
843
|
+
this.additionalHeaders = {
|
|
844
|
+
"X-Schematic-Client-Version": `schematic-js@${version}`,
|
|
845
|
+
...options?.additionalHeaders ?? {}
|
|
846
|
+
};
|
|
798
847
|
if (options?.storage) {
|
|
799
848
|
this.storage = options.storage;
|
|
800
849
|
} else if (typeof localStorage !== "undefined") {
|
|
@@ -814,6 +863,13 @@ var Schematic = class {
|
|
|
814
863
|
this.flushEventQueue();
|
|
815
864
|
});
|
|
816
865
|
}
|
|
866
|
+
if (this.offlineEnabled) {
|
|
867
|
+
this.debug(
|
|
868
|
+
"Initialized with offline mode enabled - no network requests will be made"
|
|
869
|
+
);
|
|
870
|
+
} else if (this.debugEnabled) {
|
|
871
|
+
this.debug("Initialized with debug mode enabled");
|
|
872
|
+
}
|
|
817
873
|
}
|
|
818
874
|
/**
|
|
819
875
|
* Get value for a single flag.
|
|
@@ -826,6 +882,14 @@ var Schematic = class {
|
|
|
826
882
|
const { fallback = false, key } = options;
|
|
827
883
|
const context = options.context || this.context;
|
|
828
884
|
const contextStr = contextString(context);
|
|
885
|
+
this.debug(`checkFlag: ${key}`, { context, fallback });
|
|
886
|
+
if (this.isOffline()) {
|
|
887
|
+
this.debug(`checkFlag offline result: ${key}`, {
|
|
888
|
+
value: fallback,
|
|
889
|
+
offlineMode: true
|
|
890
|
+
});
|
|
891
|
+
return fallback;
|
|
892
|
+
}
|
|
829
893
|
if (!this.useWebSocket) {
|
|
830
894
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
831
895
|
return fetch(requestUrl, {
|
|
@@ -842,17 +906,32 @@ var Schematic = class {
|
|
|
842
906
|
}
|
|
843
907
|
return response.json();
|
|
844
908
|
}).then((response) => {
|
|
845
|
-
|
|
909
|
+
const parsedResponse = CheckFlagResponseFromJSON(response);
|
|
910
|
+
this.debug(`checkFlag result: ${key}`, parsedResponse);
|
|
911
|
+
const result = CheckFlagReturnFromJSON(parsedResponse.data);
|
|
912
|
+
this.submitFlagCheckEvent(key, result, context);
|
|
913
|
+
return result.value;
|
|
846
914
|
}).catch((error) => {
|
|
847
915
|
console.error("There was a problem with the fetch operation:", error);
|
|
916
|
+
const errorResult = {
|
|
917
|
+
flag: key,
|
|
918
|
+
value: fallback,
|
|
919
|
+
reason: "API request failed",
|
|
920
|
+
error: error instanceof Error ? error.message : String(error)
|
|
921
|
+
};
|
|
922
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
848
923
|
return fallback;
|
|
849
924
|
});
|
|
850
925
|
}
|
|
851
926
|
try {
|
|
852
927
|
const existingVals = this.checks[contextStr];
|
|
853
|
-
if (this.conn && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
|
|
928
|
+
if (this.conn !== null && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
|
|
929
|
+
this.debug(`checkFlag cached result: ${key}`, existingVals[key]);
|
|
854
930
|
return existingVals[key].value;
|
|
855
931
|
}
|
|
932
|
+
if (this.isOffline()) {
|
|
933
|
+
return fallback;
|
|
934
|
+
}
|
|
856
935
|
try {
|
|
857
936
|
await this.setContext(context);
|
|
858
937
|
} catch (error) {
|
|
@@ -863,16 +942,74 @@ var Schematic = class {
|
|
|
863
942
|
return this.fallbackToRest(key, context, fallback);
|
|
864
943
|
}
|
|
865
944
|
const contextVals = this.checks[contextStr] ?? {};
|
|
866
|
-
|
|
945
|
+
const flagCheck = contextVals[key];
|
|
946
|
+
const result = flagCheck?.value ?? fallback;
|
|
947
|
+
this.debug(
|
|
948
|
+
`checkFlag WebSocket result: ${key}`,
|
|
949
|
+
typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
|
|
950
|
+
);
|
|
951
|
+
if (typeof flagCheck !== "undefined") {
|
|
952
|
+
this.submitFlagCheckEvent(key, flagCheck, context);
|
|
953
|
+
}
|
|
954
|
+
return result;
|
|
867
955
|
} catch (error) {
|
|
868
956
|
console.error("Unexpected error in checkFlag:", error);
|
|
957
|
+
const errorResult = {
|
|
958
|
+
flag: key,
|
|
959
|
+
value: fallback,
|
|
960
|
+
reason: "Unexpected error in flag check",
|
|
961
|
+
error: error instanceof Error ? error.message : String(error)
|
|
962
|
+
};
|
|
963
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
869
964
|
return fallback;
|
|
870
965
|
}
|
|
871
966
|
}
|
|
967
|
+
/**
|
|
968
|
+
* Helper function to log debug messages
|
|
969
|
+
* Only logs if debug mode is enabled
|
|
970
|
+
*/
|
|
971
|
+
debug(message, ...args) {
|
|
972
|
+
if (this.debugEnabled) {
|
|
973
|
+
console.log(`[Schematic] ${message}`, ...args);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Helper function to check if client is in offline mode
|
|
978
|
+
*/
|
|
979
|
+
isOffline() {
|
|
980
|
+
return this.offlineEnabled;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Submit a flag check event
|
|
984
|
+
* Records data about a flag check for analytics
|
|
985
|
+
*/
|
|
986
|
+
submitFlagCheckEvent(flagKey, result, context) {
|
|
987
|
+
const eventBody = {
|
|
988
|
+
flagKey,
|
|
989
|
+
value: result.value,
|
|
990
|
+
reason: result.reason,
|
|
991
|
+
flagId: result.flagId,
|
|
992
|
+
ruleId: result.ruleId,
|
|
993
|
+
companyId: result.companyId,
|
|
994
|
+
userId: result.userId,
|
|
995
|
+
error: result.error,
|
|
996
|
+
reqCompany: context.company,
|
|
997
|
+
reqUser: context.user
|
|
998
|
+
};
|
|
999
|
+
this.debug(`submitting flag check event:`, eventBody);
|
|
1000
|
+
return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
|
|
1001
|
+
}
|
|
872
1002
|
/**
|
|
873
1003
|
* Helper method for falling back to REST API when WebSocket connection fails
|
|
874
1004
|
*/
|
|
875
1005
|
async fallbackToRest(key, context, fallback) {
|
|
1006
|
+
if (this.isOffline()) {
|
|
1007
|
+
this.debug(`fallbackToRest offline result: ${key}`, {
|
|
1008
|
+
value: fallback,
|
|
1009
|
+
offlineMode: true
|
|
1010
|
+
});
|
|
1011
|
+
return fallback;
|
|
1012
|
+
}
|
|
876
1013
|
try {
|
|
877
1014
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
878
1015
|
const response = await fetch(requestUrl, {
|
|
@@ -887,19 +1024,36 @@ var Schematic = class {
|
|
|
887
1024
|
if (!response.ok) {
|
|
888
1025
|
throw new Error("Network response was not ok");
|
|
889
1026
|
}
|
|
890
|
-
const
|
|
891
|
-
|
|
1027
|
+
const responseJson = await response.json();
|
|
1028
|
+
const data = CheckFlagResponseFromJSON(responseJson);
|
|
1029
|
+
this.debug(`fallbackToRest result: ${key}`, data);
|
|
1030
|
+
const result = CheckFlagReturnFromJSON(data.data);
|
|
1031
|
+
this.submitFlagCheckEvent(key, result, context);
|
|
1032
|
+
return result.value;
|
|
892
1033
|
} catch (error) {
|
|
893
1034
|
console.error("REST API call failed, using fallback value:", error);
|
|
1035
|
+
const errorResult = {
|
|
1036
|
+
flag: key,
|
|
1037
|
+
value: fallback,
|
|
1038
|
+
reason: "API request failed (fallback)",
|
|
1039
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1040
|
+
};
|
|
1041
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
894
1042
|
return fallback;
|
|
895
1043
|
}
|
|
896
1044
|
}
|
|
897
1045
|
/**
|
|
898
1046
|
* Make an API call to fetch all flag values for a given context.
|
|
899
1047
|
* Recommended for use in REST mode only.
|
|
1048
|
+
* In offline mode, returns an empty object.
|
|
900
1049
|
*/
|
|
901
1050
|
checkFlags = async (context) => {
|
|
902
1051
|
context = context || this.context;
|
|
1052
|
+
this.debug(`checkFlags`, { context });
|
|
1053
|
+
if (this.isOffline()) {
|
|
1054
|
+
this.debug(`checkFlags offline result: returning empty object`);
|
|
1055
|
+
return {};
|
|
1056
|
+
}
|
|
903
1057
|
const requestUrl = `${this.apiUrl}/flags/check`;
|
|
904
1058
|
const requestBody = JSON.stringify(context);
|
|
905
1059
|
return fetch(requestUrl, {
|
|
@@ -917,6 +1071,7 @@ var Schematic = class {
|
|
|
917
1071
|
return response.json();
|
|
918
1072
|
}).then((responseJson) => {
|
|
919
1073
|
const resp = CheckFlagsResponseFromJSON(responseJson);
|
|
1074
|
+
this.debug(`checkFlags result:`, resp);
|
|
920
1075
|
return (resp?.data?.flags ?? []).reduce(
|
|
921
1076
|
(accum, flag) => {
|
|
922
1077
|
accum[flag.flag] = flag.value;
|
|
@@ -935,6 +1090,7 @@ var Schematic = class {
|
|
|
935
1090
|
* send an identify event to the Schematic API which will upsert a user and company.
|
|
936
1091
|
*/
|
|
937
1092
|
identify = (body) => {
|
|
1093
|
+
this.debug(`identify:`, body);
|
|
938
1094
|
try {
|
|
939
1095
|
this.setContext({
|
|
940
1096
|
company: body.company?.keys,
|
|
@@ -952,10 +1108,15 @@ var Schematic = class {
|
|
|
952
1108
|
* 2. Send the context to the server
|
|
953
1109
|
* 3. Wait for initial flag values to be returned
|
|
954
1110
|
* The promise resolves when initial flag values are received.
|
|
1111
|
+
* In offline mode, this will just set the context locally without connecting.
|
|
955
1112
|
*/
|
|
956
1113
|
setContext = async (context) => {
|
|
957
|
-
if (
|
|
1114
|
+
if (this.isOffline()) {
|
|
958
1115
|
this.context = context;
|
|
1116
|
+
this.setIsPending(false);
|
|
1117
|
+
return Promise.resolve();
|
|
1118
|
+
}
|
|
1119
|
+
if (!this.useWebSocket) {
|
|
959
1120
|
return Promise.resolve();
|
|
960
1121
|
}
|
|
961
1122
|
try {
|
|
@@ -976,12 +1137,14 @@ var Schematic = class {
|
|
|
976
1137
|
*/
|
|
977
1138
|
track = (body) => {
|
|
978
1139
|
const { company, user, event, traits } = body;
|
|
979
|
-
|
|
1140
|
+
const trackData = {
|
|
980
1141
|
company: company ?? this.context.company,
|
|
981
1142
|
event,
|
|
982
1143
|
traits: traits ?? {},
|
|
983
1144
|
user: user ?? this.context.user
|
|
984
|
-
}
|
|
1145
|
+
};
|
|
1146
|
+
this.debug(`track:`, trackData);
|
|
1147
|
+
return this.handleEvent("track", trackData);
|
|
985
1148
|
};
|
|
986
1149
|
/**
|
|
987
1150
|
* Event processing
|
|
@@ -1024,8 +1187,13 @@ var Schematic = class {
|
|
|
1024
1187
|
sendEvent = async (event) => {
|
|
1025
1188
|
const captureUrl = `${this.eventUrl}/e`;
|
|
1026
1189
|
const payload = JSON.stringify(event);
|
|
1190
|
+
this.debug(`sending event:`, { url: captureUrl, event });
|
|
1191
|
+
if (this.isOffline()) {
|
|
1192
|
+
this.debug(`event not sent (offline mode):`, { event });
|
|
1193
|
+
return Promise.resolve();
|
|
1194
|
+
}
|
|
1027
1195
|
try {
|
|
1028
|
-
await fetch(captureUrl, {
|
|
1196
|
+
const response = await fetch(captureUrl, {
|
|
1029
1197
|
method: "POST",
|
|
1030
1198
|
headers: {
|
|
1031
1199
|
...this.additionalHeaders ?? {},
|
|
@@ -1033,6 +1201,10 @@ var Schematic = class {
|
|
|
1033
1201
|
},
|
|
1034
1202
|
body: payload
|
|
1035
1203
|
});
|
|
1204
|
+
this.debug(`event sent:`, {
|
|
1205
|
+
status: response.status,
|
|
1206
|
+
statusText: response.statusText
|
|
1207
|
+
});
|
|
1036
1208
|
} catch (error) {
|
|
1037
1209
|
console.error("Error sending Schematic event: ", error);
|
|
1038
1210
|
}
|
|
@@ -1047,8 +1219,13 @@ var Schematic = class {
|
|
|
1047
1219
|
*/
|
|
1048
1220
|
/**
|
|
1049
1221
|
* If using websocket mode, close the connection when done.
|
|
1222
|
+
* In offline mode, this is a no-op.
|
|
1050
1223
|
*/
|
|
1051
1224
|
cleanup = async () => {
|
|
1225
|
+
if (this.isOffline()) {
|
|
1226
|
+
this.debug("cleanup: skipped (offline mode)");
|
|
1227
|
+
return Promise.resolve();
|
|
1228
|
+
}
|
|
1052
1229
|
if (this.conn) {
|
|
1053
1230
|
try {
|
|
1054
1231
|
const socket = await this.conn;
|
|
@@ -1062,16 +1239,26 @@ var Schematic = class {
|
|
|
1062
1239
|
};
|
|
1063
1240
|
// Open a websocket connection
|
|
1064
1241
|
wsConnect = () => {
|
|
1242
|
+
if (this.isOffline()) {
|
|
1243
|
+
this.debug("wsConnect: skipped (offline mode)");
|
|
1244
|
+
return Promise.reject(
|
|
1245
|
+
new Error("WebSocket connection skipped in offline mode")
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1065
1248
|
return new Promise((resolve, reject) => {
|
|
1066
|
-
const wsUrl = `${this.webSocketUrl}/flags/bootstrap`;
|
|
1249
|
+
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1250
|
+
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1067
1251
|
const webSocket = new WebSocket(wsUrl);
|
|
1068
1252
|
webSocket.onopen = () => {
|
|
1253
|
+
this.debug(`WebSocket connection opened`);
|
|
1069
1254
|
resolve(webSocket);
|
|
1070
1255
|
};
|
|
1071
1256
|
webSocket.onerror = (error) => {
|
|
1257
|
+
this.debug(`WebSocket connection error:`, error);
|
|
1072
1258
|
reject(error);
|
|
1073
1259
|
};
|
|
1074
1260
|
webSocket.onclose = () => {
|
|
1261
|
+
this.debug(`WebSocket connection closed`);
|
|
1075
1262
|
this.conn = null;
|
|
1076
1263
|
};
|
|
1077
1264
|
});
|
|
@@ -1079,21 +1266,37 @@ var Schematic = class {
|
|
|
1079
1266
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1080
1267
|
// and wait for the initial set of flag values to be returned
|
|
1081
1268
|
wsSendMessage = (socket, context) => {
|
|
1269
|
+
if (this.isOffline()) {
|
|
1270
|
+
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1271
|
+
this.setIsPending(false);
|
|
1272
|
+
return Promise.resolve();
|
|
1273
|
+
}
|
|
1082
1274
|
return new Promise((resolve, reject) => {
|
|
1083
1275
|
if (contextString(context) == contextString(this.context)) {
|
|
1276
|
+
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1084
1277
|
return resolve(this.setIsPending(false));
|
|
1085
1278
|
}
|
|
1279
|
+
this.debug(`WebSocket context updated:`, context);
|
|
1086
1280
|
this.context = context;
|
|
1087
1281
|
const sendMessage = () => {
|
|
1088
1282
|
let resolved = false;
|
|
1089
1283
|
const messageHandler = (event) => {
|
|
1090
1284
|
const message = JSON.parse(event.data);
|
|
1285
|
+
this.debug(`WebSocket message received:`, message);
|
|
1091
1286
|
if (!(contextString(context) in this.checks)) {
|
|
1092
1287
|
this.checks[contextString(context)] = {};
|
|
1093
1288
|
}
|
|
1094
1289
|
(message.flags ?? []).forEach((flag) => {
|
|
1095
1290
|
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1096
1291
|
this.checks[contextString(context)][flagCheck.flag] = flagCheck;
|
|
1292
|
+
this.debug(`WebSocket flag update:`, {
|
|
1293
|
+
flag: flagCheck.flag,
|
|
1294
|
+
value: flagCheck.value,
|
|
1295
|
+
flagCheck
|
|
1296
|
+
});
|
|
1297
|
+
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1298
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1299
|
+
}
|
|
1097
1300
|
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1098
1301
|
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1099
1302
|
});
|
|
@@ -1104,18 +1307,23 @@ var Schematic = class {
|
|
|
1104
1307
|
}
|
|
1105
1308
|
};
|
|
1106
1309
|
socket.addEventListener("message", messageHandler);
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1310
|
+
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1311
|
+
const messagePayload = {
|
|
1312
|
+
apiKey: this.apiKey,
|
|
1313
|
+
clientVersion,
|
|
1314
|
+
data: context
|
|
1315
|
+
};
|
|
1316
|
+
this.debug(`WebSocket sending message:`, messagePayload);
|
|
1317
|
+
socket.send(JSON.stringify(messagePayload));
|
|
1113
1318
|
};
|
|
1114
1319
|
if (socket.readyState === WebSocket.OPEN) {
|
|
1320
|
+
this.debug(`WebSocket already open, sending message`);
|
|
1115
1321
|
sendMessage();
|
|
1116
1322
|
} else if (socket.readyState === WebSocket.CONNECTING) {
|
|
1323
|
+
this.debug(`WebSocket connecting, waiting for open to send message`);
|
|
1117
1324
|
socket.addEventListener("open", sendMessage);
|
|
1118
1325
|
} else {
|
|
1326
|
+
this.debug(`WebSocket is closed, cannot send message`);
|
|
1119
1327
|
reject("WebSocket is not open or connecting");
|
|
1120
1328
|
}
|
|
1121
1329
|
});
|
|
@@ -1172,10 +1380,22 @@ var Schematic = class {
|
|
|
1172
1380
|
};
|
|
1173
1381
|
notifyFlagCheckListeners = (flagKey, check) => {
|
|
1174
1382
|
const listeners = this.flagCheckListeners?.[flagKey] ?? [];
|
|
1383
|
+
if (listeners.size > 0) {
|
|
1384
|
+
this.debug(
|
|
1385
|
+
`Notifying ${listeners.size} flag check listeners for ${flagKey}`,
|
|
1386
|
+
check
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1175
1389
|
listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
|
|
1176
1390
|
};
|
|
1177
1391
|
notifyFlagValueListeners = (flagKey, value) => {
|
|
1178
1392
|
const listeners = this.flagValueListeners?.[flagKey] ?? [];
|
|
1393
|
+
if (listeners.size > 0) {
|
|
1394
|
+
this.debug(
|
|
1395
|
+
`Notifying ${listeners.size} flag value listeners for ${flagKey}`,
|
|
1396
|
+
{ value }
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1179
1399
|
listeners.forEach((listener) => notifyFlagValueListener(listener, value));
|
|
1180
1400
|
};
|
|
1181
1401
|
};
|
package/dist/schematic.d.ts
CHANGED
|
@@ -208,7 +208,92 @@ declare type Event_2 = {
|
|
|
208
208
|
};
|
|
209
209
|
export { Event_2 as Event }
|
|
210
210
|
|
|
211
|
-
export declare type EventBody = EventBodyIdentify | EventBodyTrack;
|
|
211
|
+
export declare type EventBody = EventBodyIdentify | EventBodyTrack | EventBodyFlagCheck;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Schematic API
|
|
215
|
+
* Schematic API
|
|
216
|
+
*
|
|
217
|
+
* The version of the OpenAPI document: 0.1
|
|
218
|
+
*
|
|
219
|
+
*
|
|
220
|
+
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
|
221
|
+
* https://openapi-generator.tech
|
|
222
|
+
* Do not edit the class manually.
|
|
223
|
+
*/
|
|
224
|
+
/**
|
|
225
|
+
*
|
|
226
|
+
* @export
|
|
227
|
+
* @interface EventBodyFlagCheck
|
|
228
|
+
*/
|
|
229
|
+
export declare interface EventBodyFlagCheck {
|
|
230
|
+
/**
|
|
231
|
+
* Schematic company ID (starting with 'comp_') of the company evaluated, if any
|
|
232
|
+
* @type {string}
|
|
233
|
+
* @memberof EventBodyFlagCheck
|
|
234
|
+
*/
|
|
235
|
+
companyId?: string | null;
|
|
236
|
+
/**
|
|
237
|
+
* Report an error that occurred during the flag check
|
|
238
|
+
* @type {string}
|
|
239
|
+
* @memberof EventBodyFlagCheck
|
|
240
|
+
*/
|
|
241
|
+
error?: string | null;
|
|
242
|
+
/**
|
|
243
|
+
* Schematic flag ID (starting with 'flag_') for the flag matching the key, if any
|
|
244
|
+
* @type {string}
|
|
245
|
+
* @memberof EventBodyFlagCheck
|
|
246
|
+
*/
|
|
247
|
+
flagId?: string | null;
|
|
248
|
+
/**
|
|
249
|
+
* The key of the flag being checked
|
|
250
|
+
* @type {string}
|
|
251
|
+
* @memberof EventBodyFlagCheck
|
|
252
|
+
*/
|
|
253
|
+
flagKey: string;
|
|
254
|
+
/**
|
|
255
|
+
* The reason why the value was returned
|
|
256
|
+
* @type {string}
|
|
257
|
+
* @memberof EventBodyFlagCheck
|
|
258
|
+
*/
|
|
259
|
+
reason: string;
|
|
260
|
+
/**
|
|
261
|
+
* Key-value pairs used to to identify company for which the flag was checked
|
|
262
|
+
* @type {{ [key: string]: string; }}
|
|
263
|
+
* @memberof EventBodyFlagCheck
|
|
264
|
+
*/
|
|
265
|
+
reqCompany?: {
|
|
266
|
+
[key: string]: string;
|
|
267
|
+
} | null;
|
|
268
|
+
/**
|
|
269
|
+
* Key-value pairs used to to identify user for which the flag was checked
|
|
270
|
+
* @type {{ [key: string]: string; }}
|
|
271
|
+
* @memberof EventBodyFlagCheck
|
|
272
|
+
*/
|
|
273
|
+
reqUser?: {
|
|
274
|
+
[key: string]: string;
|
|
275
|
+
} | null;
|
|
276
|
+
/**
|
|
277
|
+
* Schematic rule ID (starting with 'rule_') of the rule that matched for the flag, if any
|
|
278
|
+
* @type {string}
|
|
279
|
+
* @memberof EventBodyFlagCheck
|
|
280
|
+
*/
|
|
281
|
+
ruleId?: string | null;
|
|
282
|
+
/**
|
|
283
|
+
* Schematic user ID (starting with 'user_') of the user evaluated, if any
|
|
284
|
+
* @type {string}
|
|
285
|
+
* @memberof EventBodyFlagCheck
|
|
286
|
+
*/
|
|
287
|
+
userId?: string | null;
|
|
288
|
+
/**
|
|
289
|
+
* The value of the flag for the given company and/or user
|
|
290
|
+
* @type {boolean}
|
|
291
|
+
* @memberof EventBodyFlagCheck
|
|
292
|
+
*/
|
|
293
|
+
value: boolean;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export declare function EventBodyFlagCheckToJSON(json: any): EventBodyFlagCheck;
|
|
212
297
|
|
|
213
298
|
export declare type EventBodyIdentify = {
|
|
214
299
|
company?: {
|
|
@@ -227,7 +312,7 @@ export declare type EventBodyTrack = SchematicContext & {
|
|
|
227
312
|
traits?: Traits;
|
|
228
313
|
};
|
|
229
314
|
|
|
230
|
-
export declare type EventType = "identify" | "track";
|
|
315
|
+
export declare type EventType = "identify" | "track" | "flag_check";
|
|
231
316
|
|
|
232
317
|
export declare type FlagCheckListenerFn = CheckFlagReturnListenerFn | EmptyListenerFn;
|
|
233
318
|
|
|
@@ -261,6 +346,8 @@ export declare class Schematic {
|
|
|
261
346
|
private apiUrl;
|
|
262
347
|
private conn;
|
|
263
348
|
private context;
|
|
349
|
+
private debugEnabled;
|
|
350
|
+
private offlineEnabled;
|
|
264
351
|
private eventQueue;
|
|
265
352
|
private eventUrl;
|
|
266
353
|
private flagCheckListeners;
|
|
@@ -280,6 +367,20 @@ export declare class Schematic {
|
|
|
280
367
|
* In REST mode, makes an API call for each check.
|
|
281
368
|
*/
|
|
282
369
|
checkFlag(options: CheckOptions): Promise<boolean>;
|
|
370
|
+
/**
|
|
371
|
+
* Helper function to log debug messages
|
|
372
|
+
* Only logs if debug mode is enabled
|
|
373
|
+
*/
|
|
374
|
+
private debug;
|
|
375
|
+
/**
|
|
376
|
+
* Helper function to check if client is in offline mode
|
|
377
|
+
*/
|
|
378
|
+
private isOffline;
|
|
379
|
+
/**
|
|
380
|
+
* Submit a flag check event
|
|
381
|
+
* Records data about a flag check for analytics
|
|
382
|
+
*/
|
|
383
|
+
private submitFlagCheckEvent;
|
|
283
384
|
/**
|
|
284
385
|
* Helper method for falling back to REST API when WebSocket connection fails
|
|
285
386
|
*/
|
|
@@ -287,6 +388,7 @@ export declare class Schematic {
|
|
|
287
388
|
/**
|
|
288
389
|
* Make an API call to fetch all flag values for a given context.
|
|
289
390
|
* Recommended for use in REST mode only.
|
|
391
|
+
* In offline mode, returns an empty object.
|
|
290
392
|
*/
|
|
291
393
|
checkFlags: (context?: SchematicContext) => Promise<Record<string, boolean>>;
|
|
292
394
|
/**
|
|
@@ -302,6 +404,7 @@ export declare class Schematic {
|
|
|
302
404
|
* 2. Send the context to the server
|
|
303
405
|
* 3. Wait for initial flag values to be returned
|
|
304
406
|
* The promise resolves when initial flag values are received.
|
|
407
|
+
* In offline mode, this will just set the context locally without connecting.
|
|
305
408
|
*/
|
|
306
409
|
setContext: (context: SchematicContext) => Promise<void>;
|
|
307
410
|
/**
|
|
@@ -322,6 +425,7 @@ export declare class Schematic {
|
|
|
322
425
|
*/
|
|
323
426
|
/**
|
|
324
427
|
* If using websocket mode, close the connection when done.
|
|
428
|
+
* In offline mode, this is a no-op.
|
|
325
429
|
*/
|
|
326
430
|
cleanup: () => Promise<void>;
|
|
327
431
|
private wsConnect;
|
|
@@ -353,8 +457,15 @@ export declare type SchematicOptions = {
|
|
|
353
457
|
additionalHeaders?: Record<string, string>;
|
|
354
458
|
/** Optionally provide a custom API URL */
|
|
355
459
|
apiUrl?: string;
|
|
460
|
+
/** Enable debug mode to log flag check results and events to the console.
|
|
461
|
+
* Can also be enabled at runtime via URL query parameter "schematic_debug=true" */
|
|
462
|
+
debug?: boolean;
|
|
356
463
|
/** Optionally provide a custom event URL */
|
|
357
464
|
eventUrl?: string;
|
|
465
|
+
/** Enable offline mode to prevent all network requests.
|
|
466
|
+
* When enabled, events are only logged not sent, and flag checks return fallback values.
|
|
467
|
+
* Can also be enabled at runtime via URL query parameter "schematic_offline=true" */
|
|
468
|
+
offline?: boolean;
|
|
358
469
|
/** Optionally provide a custom storage persister for client-side storage */
|
|
359
470
|
storage?: StoragePersister;
|
|
360
471
|
/** Use a WebSocket connection for real-time flag checks; if using this, run the cleanup function to close the connection */
|
package/dist/schematic.esm.js
CHANGED
|
@@ -640,6 +640,28 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
|
|
|
640
640
|
};
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
+
// src/types/api/models/EventBodyFlagCheck.ts
|
|
644
|
+
function EventBodyFlagCheckToJSON(json) {
|
|
645
|
+
return EventBodyFlagCheckToJSONTyped(json, false);
|
|
646
|
+
}
|
|
647
|
+
function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
|
|
648
|
+
if (value == null) {
|
|
649
|
+
return value;
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
company_id: value["companyId"],
|
|
653
|
+
error: value["error"],
|
|
654
|
+
flag_id: value["flagId"],
|
|
655
|
+
flag_key: value["flagKey"],
|
|
656
|
+
reason: value["reason"],
|
|
657
|
+
req_company: value["reqCompany"],
|
|
658
|
+
req_user: value["reqUser"],
|
|
659
|
+
rule_id: value["ruleId"],
|
|
660
|
+
user_id: value["userId"],
|
|
661
|
+
value: value["value"]
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
643
665
|
// src/types/api/models/CheckFlagResponse.ts
|
|
644
666
|
function CheckFlagResponseFromJSON(json) {
|
|
645
667
|
return CheckFlagResponseFromJSONTyped(json, false);
|
|
@@ -752,6 +774,9 @@ function contextString(context) {
|
|
|
752
774
|
return JSON.stringify(sortedContext);
|
|
753
775
|
}
|
|
754
776
|
|
|
777
|
+
// src/version.ts
|
|
778
|
+
var version = "1.2.2";
|
|
779
|
+
|
|
755
780
|
// src/index.ts
|
|
756
781
|
var anonymousIdKey = "schematicId";
|
|
757
782
|
var Schematic = class {
|
|
@@ -760,6 +785,8 @@ var Schematic = class {
|
|
|
760
785
|
apiUrl = "https://api.schematichq.com";
|
|
761
786
|
conn = null;
|
|
762
787
|
context = {};
|
|
788
|
+
debugEnabled = false;
|
|
789
|
+
offlineEnabled = false;
|
|
763
790
|
eventQueue;
|
|
764
791
|
eventUrl = "https://c.schematichq.com";
|
|
765
792
|
flagCheckListeners = {};
|
|
@@ -774,9 +801,30 @@ var Schematic = class {
|
|
|
774
801
|
this.apiKey = apiKey;
|
|
775
802
|
this.eventQueue = [];
|
|
776
803
|
this.useWebSocket = options?.useWebSocket ?? false;
|
|
777
|
-
|
|
778
|
-
|
|
804
|
+
this.debugEnabled = options?.debug ?? false;
|
|
805
|
+
this.offlineEnabled = options?.offline ?? false;
|
|
806
|
+
if (typeof window !== "undefined" && typeof window.location !== "undefined") {
|
|
807
|
+
const params = new URLSearchParams(window.location.search);
|
|
808
|
+
const debugParam = params.get("schematic_debug");
|
|
809
|
+
if (debugParam !== null && (debugParam === "" || debugParam === "true" || debugParam === "1")) {
|
|
810
|
+
this.debugEnabled = true;
|
|
811
|
+
}
|
|
812
|
+
const offlineParam = params.get("schematic_offline");
|
|
813
|
+
if (offlineParam !== null && (offlineParam === "" || offlineParam === "true" || offlineParam === "1")) {
|
|
814
|
+
this.offlineEnabled = true;
|
|
815
|
+
this.debugEnabled = true;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (this.offlineEnabled && options?.debug !== false) {
|
|
819
|
+
this.debugEnabled = true;
|
|
779
820
|
}
|
|
821
|
+
if (this.offlineEnabled) {
|
|
822
|
+
this.setIsPending(false);
|
|
823
|
+
}
|
|
824
|
+
this.additionalHeaders = {
|
|
825
|
+
"X-Schematic-Client-Version": `schematic-js@${version}`,
|
|
826
|
+
...options?.additionalHeaders ?? {}
|
|
827
|
+
};
|
|
780
828
|
if (options?.storage) {
|
|
781
829
|
this.storage = options.storage;
|
|
782
830
|
} else if (typeof localStorage !== "undefined") {
|
|
@@ -796,6 +844,13 @@ var Schematic = class {
|
|
|
796
844
|
this.flushEventQueue();
|
|
797
845
|
});
|
|
798
846
|
}
|
|
847
|
+
if (this.offlineEnabled) {
|
|
848
|
+
this.debug(
|
|
849
|
+
"Initialized with offline mode enabled - no network requests will be made"
|
|
850
|
+
);
|
|
851
|
+
} else if (this.debugEnabled) {
|
|
852
|
+
this.debug("Initialized with debug mode enabled");
|
|
853
|
+
}
|
|
799
854
|
}
|
|
800
855
|
/**
|
|
801
856
|
* Get value for a single flag.
|
|
@@ -808,6 +863,14 @@ var Schematic = class {
|
|
|
808
863
|
const { fallback = false, key } = options;
|
|
809
864
|
const context = options.context || this.context;
|
|
810
865
|
const contextStr = contextString(context);
|
|
866
|
+
this.debug(`checkFlag: ${key}`, { context, fallback });
|
|
867
|
+
if (this.isOffline()) {
|
|
868
|
+
this.debug(`checkFlag offline result: ${key}`, {
|
|
869
|
+
value: fallback,
|
|
870
|
+
offlineMode: true
|
|
871
|
+
});
|
|
872
|
+
return fallback;
|
|
873
|
+
}
|
|
811
874
|
if (!this.useWebSocket) {
|
|
812
875
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
813
876
|
return fetch(requestUrl, {
|
|
@@ -824,17 +887,32 @@ var Schematic = class {
|
|
|
824
887
|
}
|
|
825
888
|
return response.json();
|
|
826
889
|
}).then((response) => {
|
|
827
|
-
|
|
890
|
+
const parsedResponse = CheckFlagResponseFromJSON(response);
|
|
891
|
+
this.debug(`checkFlag result: ${key}`, parsedResponse);
|
|
892
|
+
const result = CheckFlagReturnFromJSON(parsedResponse.data);
|
|
893
|
+
this.submitFlagCheckEvent(key, result, context);
|
|
894
|
+
return result.value;
|
|
828
895
|
}).catch((error) => {
|
|
829
896
|
console.error("There was a problem with the fetch operation:", error);
|
|
897
|
+
const errorResult = {
|
|
898
|
+
flag: key,
|
|
899
|
+
value: fallback,
|
|
900
|
+
reason: "API request failed",
|
|
901
|
+
error: error instanceof Error ? error.message : String(error)
|
|
902
|
+
};
|
|
903
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
830
904
|
return fallback;
|
|
831
905
|
});
|
|
832
906
|
}
|
|
833
907
|
try {
|
|
834
908
|
const existingVals = this.checks[contextStr];
|
|
835
|
-
if (this.conn && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
|
|
909
|
+
if (this.conn !== null && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
|
|
910
|
+
this.debug(`checkFlag cached result: ${key}`, existingVals[key]);
|
|
836
911
|
return existingVals[key].value;
|
|
837
912
|
}
|
|
913
|
+
if (this.isOffline()) {
|
|
914
|
+
return fallback;
|
|
915
|
+
}
|
|
838
916
|
try {
|
|
839
917
|
await this.setContext(context);
|
|
840
918
|
} catch (error) {
|
|
@@ -845,16 +923,74 @@ var Schematic = class {
|
|
|
845
923
|
return this.fallbackToRest(key, context, fallback);
|
|
846
924
|
}
|
|
847
925
|
const contextVals = this.checks[contextStr] ?? {};
|
|
848
|
-
|
|
926
|
+
const flagCheck = contextVals[key];
|
|
927
|
+
const result = flagCheck?.value ?? fallback;
|
|
928
|
+
this.debug(
|
|
929
|
+
`checkFlag WebSocket result: ${key}`,
|
|
930
|
+
typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
|
|
931
|
+
);
|
|
932
|
+
if (typeof flagCheck !== "undefined") {
|
|
933
|
+
this.submitFlagCheckEvent(key, flagCheck, context);
|
|
934
|
+
}
|
|
935
|
+
return result;
|
|
849
936
|
} catch (error) {
|
|
850
937
|
console.error("Unexpected error in checkFlag:", error);
|
|
938
|
+
const errorResult = {
|
|
939
|
+
flag: key,
|
|
940
|
+
value: fallback,
|
|
941
|
+
reason: "Unexpected error in flag check",
|
|
942
|
+
error: error instanceof Error ? error.message : String(error)
|
|
943
|
+
};
|
|
944
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
851
945
|
return fallback;
|
|
852
946
|
}
|
|
853
947
|
}
|
|
948
|
+
/**
|
|
949
|
+
* Helper function to log debug messages
|
|
950
|
+
* Only logs if debug mode is enabled
|
|
951
|
+
*/
|
|
952
|
+
debug(message, ...args) {
|
|
953
|
+
if (this.debugEnabled) {
|
|
954
|
+
console.log(`[Schematic] ${message}`, ...args);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Helper function to check if client is in offline mode
|
|
959
|
+
*/
|
|
960
|
+
isOffline() {
|
|
961
|
+
return this.offlineEnabled;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Submit a flag check event
|
|
965
|
+
* Records data about a flag check for analytics
|
|
966
|
+
*/
|
|
967
|
+
submitFlagCheckEvent(flagKey, result, context) {
|
|
968
|
+
const eventBody = {
|
|
969
|
+
flagKey,
|
|
970
|
+
value: result.value,
|
|
971
|
+
reason: result.reason,
|
|
972
|
+
flagId: result.flagId,
|
|
973
|
+
ruleId: result.ruleId,
|
|
974
|
+
companyId: result.companyId,
|
|
975
|
+
userId: result.userId,
|
|
976
|
+
error: result.error,
|
|
977
|
+
reqCompany: context.company,
|
|
978
|
+
reqUser: context.user
|
|
979
|
+
};
|
|
980
|
+
this.debug(`submitting flag check event:`, eventBody);
|
|
981
|
+
return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
|
|
982
|
+
}
|
|
854
983
|
/**
|
|
855
984
|
* Helper method for falling back to REST API when WebSocket connection fails
|
|
856
985
|
*/
|
|
857
986
|
async fallbackToRest(key, context, fallback) {
|
|
987
|
+
if (this.isOffline()) {
|
|
988
|
+
this.debug(`fallbackToRest offline result: ${key}`, {
|
|
989
|
+
value: fallback,
|
|
990
|
+
offlineMode: true
|
|
991
|
+
});
|
|
992
|
+
return fallback;
|
|
993
|
+
}
|
|
858
994
|
try {
|
|
859
995
|
const requestUrl = `${this.apiUrl}/flags/${key}/check`;
|
|
860
996
|
const response = await fetch(requestUrl, {
|
|
@@ -869,19 +1005,36 @@ var Schematic = class {
|
|
|
869
1005
|
if (!response.ok) {
|
|
870
1006
|
throw new Error("Network response was not ok");
|
|
871
1007
|
}
|
|
872
|
-
const
|
|
873
|
-
|
|
1008
|
+
const responseJson = await response.json();
|
|
1009
|
+
const data = CheckFlagResponseFromJSON(responseJson);
|
|
1010
|
+
this.debug(`fallbackToRest result: ${key}`, data);
|
|
1011
|
+
const result = CheckFlagReturnFromJSON(data.data);
|
|
1012
|
+
this.submitFlagCheckEvent(key, result, context);
|
|
1013
|
+
return result.value;
|
|
874
1014
|
} catch (error) {
|
|
875
1015
|
console.error("REST API call failed, using fallback value:", error);
|
|
1016
|
+
const errorResult = {
|
|
1017
|
+
flag: key,
|
|
1018
|
+
value: fallback,
|
|
1019
|
+
reason: "API request failed (fallback)",
|
|
1020
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1021
|
+
};
|
|
1022
|
+
this.submitFlagCheckEvent(key, errorResult, context);
|
|
876
1023
|
return fallback;
|
|
877
1024
|
}
|
|
878
1025
|
}
|
|
879
1026
|
/**
|
|
880
1027
|
* Make an API call to fetch all flag values for a given context.
|
|
881
1028
|
* Recommended for use in REST mode only.
|
|
1029
|
+
* In offline mode, returns an empty object.
|
|
882
1030
|
*/
|
|
883
1031
|
checkFlags = async (context) => {
|
|
884
1032
|
context = context || this.context;
|
|
1033
|
+
this.debug(`checkFlags`, { context });
|
|
1034
|
+
if (this.isOffline()) {
|
|
1035
|
+
this.debug(`checkFlags offline result: returning empty object`);
|
|
1036
|
+
return {};
|
|
1037
|
+
}
|
|
885
1038
|
const requestUrl = `${this.apiUrl}/flags/check`;
|
|
886
1039
|
const requestBody = JSON.stringify(context);
|
|
887
1040
|
return fetch(requestUrl, {
|
|
@@ -899,6 +1052,7 @@ var Schematic = class {
|
|
|
899
1052
|
return response.json();
|
|
900
1053
|
}).then((responseJson) => {
|
|
901
1054
|
const resp = CheckFlagsResponseFromJSON(responseJson);
|
|
1055
|
+
this.debug(`checkFlags result:`, resp);
|
|
902
1056
|
return (resp?.data?.flags ?? []).reduce(
|
|
903
1057
|
(accum, flag) => {
|
|
904
1058
|
accum[flag.flag] = flag.value;
|
|
@@ -917,6 +1071,7 @@ var Schematic = class {
|
|
|
917
1071
|
* send an identify event to the Schematic API which will upsert a user and company.
|
|
918
1072
|
*/
|
|
919
1073
|
identify = (body) => {
|
|
1074
|
+
this.debug(`identify:`, body);
|
|
920
1075
|
try {
|
|
921
1076
|
this.setContext({
|
|
922
1077
|
company: body.company?.keys,
|
|
@@ -934,10 +1089,15 @@ var Schematic = class {
|
|
|
934
1089
|
* 2. Send the context to the server
|
|
935
1090
|
* 3. Wait for initial flag values to be returned
|
|
936
1091
|
* The promise resolves when initial flag values are received.
|
|
1092
|
+
* In offline mode, this will just set the context locally without connecting.
|
|
937
1093
|
*/
|
|
938
1094
|
setContext = async (context) => {
|
|
939
|
-
if (
|
|
1095
|
+
if (this.isOffline()) {
|
|
940
1096
|
this.context = context;
|
|
1097
|
+
this.setIsPending(false);
|
|
1098
|
+
return Promise.resolve();
|
|
1099
|
+
}
|
|
1100
|
+
if (!this.useWebSocket) {
|
|
941
1101
|
return Promise.resolve();
|
|
942
1102
|
}
|
|
943
1103
|
try {
|
|
@@ -958,12 +1118,14 @@ var Schematic = class {
|
|
|
958
1118
|
*/
|
|
959
1119
|
track = (body) => {
|
|
960
1120
|
const { company, user, event, traits } = body;
|
|
961
|
-
|
|
1121
|
+
const trackData = {
|
|
962
1122
|
company: company ?? this.context.company,
|
|
963
1123
|
event,
|
|
964
1124
|
traits: traits ?? {},
|
|
965
1125
|
user: user ?? this.context.user
|
|
966
|
-
}
|
|
1126
|
+
};
|
|
1127
|
+
this.debug(`track:`, trackData);
|
|
1128
|
+
return this.handleEvent("track", trackData);
|
|
967
1129
|
};
|
|
968
1130
|
/**
|
|
969
1131
|
* Event processing
|
|
@@ -1006,8 +1168,13 @@ var Schematic = class {
|
|
|
1006
1168
|
sendEvent = async (event) => {
|
|
1007
1169
|
const captureUrl = `${this.eventUrl}/e`;
|
|
1008
1170
|
const payload = JSON.stringify(event);
|
|
1171
|
+
this.debug(`sending event:`, { url: captureUrl, event });
|
|
1172
|
+
if (this.isOffline()) {
|
|
1173
|
+
this.debug(`event not sent (offline mode):`, { event });
|
|
1174
|
+
return Promise.resolve();
|
|
1175
|
+
}
|
|
1009
1176
|
try {
|
|
1010
|
-
await fetch(captureUrl, {
|
|
1177
|
+
const response = await fetch(captureUrl, {
|
|
1011
1178
|
method: "POST",
|
|
1012
1179
|
headers: {
|
|
1013
1180
|
...this.additionalHeaders ?? {},
|
|
@@ -1015,6 +1182,10 @@ var Schematic = class {
|
|
|
1015
1182
|
},
|
|
1016
1183
|
body: payload
|
|
1017
1184
|
});
|
|
1185
|
+
this.debug(`event sent:`, {
|
|
1186
|
+
status: response.status,
|
|
1187
|
+
statusText: response.statusText
|
|
1188
|
+
});
|
|
1018
1189
|
} catch (error) {
|
|
1019
1190
|
console.error("Error sending Schematic event: ", error);
|
|
1020
1191
|
}
|
|
@@ -1029,8 +1200,13 @@ var Schematic = class {
|
|
|
1029
1200
|
*/
|
|
1030
1201
|
/**
|
|
1031
1202
|
* If using websocket mode, close the connection when done.
|
|
1203
|
+
* In offline mode, this is a no-op.
|
|
1032
1204
|
*/
|
|
1033
1205
|
cleanup = async () => {
|
|
1206
|
+
if (this.isOffline()) {
|
|
1207
|
+
this.debug("cleanup: skipped (offline mode)");
|
|
1208
|
+
return Promise.resolve();
|
|
1209
|
+
}
|
|
1034
1210
|
if (this.conn) {
|
|
1035
1211
|
try {
|
|
1036
1212
|
const socket = await this.conn;
|
|
@@ -1044,16 +1220,26 @@ var Schematic = class {
|
|
|
1044
1220
|
};
|
|
1045
1221
|
// Open a websocket connection
|
|
1046
1222
|
wsConnect = () => {
|
|
1223
|
+
if (this.isOffline()) {
|
|
1224
|
+
this.debug("wsConnect: skipped (offline mode)");
|
|
1225
|
+
return Promise.reject(
|
|
1226
|
+
new Error("WebSocket connection skipped in offline mode")
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1047
1229
|
return new Promise((resolve, reject) => {
|
|
1048
|
-
const wsUrl = `${this.webSocketUrl}/flags/bootstrap`;
|
|
1230
|
+
const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
|
|
1231
|
+
this.debug(`connecting to WebSocket:`, wsUrl);
|
|
1049
1232
|
const webSocket = new WebSocket(wsUrl);
|
|
1050
1233
|
webSocket.onopen = () => {
|
|
1234
|
+
this.debug(`WebSocket connection opened`);
|
|
1051
1235
|
resolve(webSocket);
|
|
1052
1236
|
};
|
|
1053
1237
|
webSocket.onerror = (error) => {
|
|
1238
|
+
this.debug(`WebSocket connection error:`, error);
|
|
1054
1239
|
reject(error);
|
|
1055
1240
|
};
|
|
1056
1241
|
webSocket.onclose = () => {
|
|
1242
|
+
this.debug(`WebSocket connection closed`);
|
|
1057
1243
|
this.conn = null;
|
|
1058
1244
|
};
|
|
1059
1245
|
});
|
|
@@ -1061,21 +1247,37 @@ var Schematic = class {
|
|
|
1061
1247
|
// Send a message on the websocket indicating interest in a particular evaluation context
|
|
1062
1248
|
// and wait for the initial set of flag values to be returned
|
|
1063
1249
|
wsSendMessage = (socket, context) => {
|
|
1250
|
+
if (this.isOffline()) {
|
|
1251
|
+
this.debug("wsSendMessage: skipped (offline mode)");
|
|
1252
|
+
this.setIsPending(false);
|
|
1253
|
+
return Promise.resolve();
|
|
1254
|
+
}
|
|
1064
1255
|
return new Promise((resolve, reject) => {
|
|
1065
1256
|
if (contextString(context) == contextString(this.context)) {
|
|
1257
|
+
this.debug(`WebSocket context unchanged, skipping update`);
|
|
1066
1258
|
return resolve(this.setIsPending(false));
|
|
1067
1259
|
}
|
|
1260
|
+
this.debug(`WebSocket context updated:`, context);
|
|
1068
1261
|
this.context = context;
|
|
1069
1262
|
const sendMessage = () => {
|
|
1070
1263
|
let resolved = false;
|
|
1071
1264
|
const messageHandler = (event) => {
|
|
1072
1265
|
const message = JSON.parse(event.data);
|
|
1266
|
+
this.debug(`WebSocket message received:`, message);
|
|
1073
1267
|
if (!(contextString(context) in this.checks)) {
|
|
1074
1268
|
this.checks[contextString(context)] = {};
|
|
1075
1269
|
}
|
|
1076
1270
|
(message.flags ?? []).forEach((flag) => {
|
|
1077
1271
|
const flagCheck = CheckFlagReturnFromJSON(flag);
|
|
1078
1272
|
this.checks[contextString(context)][flagCheck.flag] = flagCheck;
|
|
1273
|
+
this.debug(`WebSocket flag update:`, {
|
|
1274
|
+
flag: flagCheck.flag,
|
|
1275
|
+
value: flagCheck.value,
|
|
1276
|
+
flagCheck
|
|
1277
|
+
});
|
|
1278
|
+
if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
|
|
1279
|
+
this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
|
|
1280
|
+
}
|
|
1079
1281
|
this.notifyFlagCheckListeners(flag.flag, flagCheck);
|
|
1080
1282
|
this.notifyFlagValueListeners(flag.flag, flagCheck.value);
|
|
1081
1283
|
});
|
|
@@ -1086,18 +1288,23 @@ var Schematic = class {
|
|
|
1086
1288
|
}
|
|
1087
1289
|
};
|
|
1088
1290
|
socket.addEventListener("message", messageHandler);
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1291
|
+
const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
|
|
1292
|
+
const messagePayload = {
|
|
1293
|
+
apiKey: this.apiKey,
|
|
1294
|
+
clientVersion,
|
|
1295
|
+
data: context
|
|
1296
|
+
};
|
|
1297
|
+
this.debug(`WebSocket sending message:`, messagePayload);
|
|
1298
|
+
socket.send(JSON.stringify(messagePayload));
|
|
1095
1299
|
};
|
|
1096
1300
|
if (socket.readyState === WebSocket.OPEN) {
|
|
1301
|
+
this.debug(`WebSocket already open, sending message`);
|
|
1097
1302
|
sendMessage();
|
|
1098
1303
|
} else if (socket.readyState === WebSocket.CONNECTING) {
|
|
1304
|
+
this.debug(`WebSocket connecting, waiting for open to send message`);
|
|
1099
1305
|
socket.addEventListener("open", sendMessage);
|
|
1100
1306
|
} else {
|
|
1307
|
+
this.debug(`WebSocket is closed, cannot send message`);
|
|
1101
1308
|
reject("WebSocket is not open or connecting");
|
|
1102
1309
|
}
|
|
1103
1310
|
});
|
|
@@ -1154,10 +1361,22 @@ var Schematic = class {
|
|
|
1154
1361
|
};
|
|
1155
1362
|
notifyFlagCheckListeners = (flagKey, check) => {
|
|
1156
1363
|
const listeners = this.flagCheckListeners?.[flagKey] ?? [];
|
|
1364
|
+
if (listeners.size > 0) {
|
|
1365
|
+
this.debug(
|
|
1366
|
+
`Notifying ${listeners.size} flag check listeners for ${flagKey}`,
|
|
1367
|
+
check
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1157
1370
|
listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
|
|
1158
1371
|
};
|
|
1159
1372
|
notifyFlagValueListeners = (flagKey, value) => {
|
|
1160
1373
|
const listeners = this.flagValueListeners?.[flagKey] ?? [];
|
|
1374
|
+
if (listeners.size > 0) {
|
|
1375
|
+
this.debug(
|
|
1376
|
+
`Notifying ${listeners.size} flag value listeners for ${flagKey}`,
|
|
1377
|
+
{ value }
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1161
1380
|
listeners.forEach((listener) => notifyFlagValueListener(listener, value));
|
|
1162
1381
|
};
|
|
1163
1382
|
};
|
|
@@ -1186,6 +1405,7 @@ export {
|
|
|
1186
1405
|
CheckFlagResponseFromJSON,
|
|
1187
1406
|
CheckFlagReturnFromJSON,
|
|
1188
1407
|
CheckFlagsResponseFromJSON,
|
|
1408
|
+
EventBodyFlagCheckToJSON,
|
|
1189
1409
|
RuleType,
|
|
1190
1410
|
Schematic,
|
|
1191
1411
|
UsagePeriod
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schematichq/schematic-js",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"main": "dist/schematic.cjs.js",
|
|
5
5
|
"module": "dist/schematic.esm.js",
|
|
6
6
|
"types": "dist/schematic.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "git+ssh://git@github.com/SchematicHQ/schematic-js.git"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "npx tsc && yarn clean && yarn build:browser && yarn build:cjs && yarn build:esm && yarn build:types",
|
|
20
|
+
"build": "./version.sh && npx tsc && yarn clean && yarn build:browser && yarn build:cjs && yarn build:esm && yarn build:types",
|
|
21
21
|
"build:browser": "npx esbuild src/browser.ts --bundle --minify --outfile=dist/schematic.browser.js --platform=browser",
|
|
22
22
|
"build:cjs": "npx esbuild src/index.ts --bundle --format=cjs --outfile=dist/schematic.cjs.js",
|
|
23
23
|
"build:esm": "npx esbuild src/index.ts --bundle --format=esm --outfile=dist/schematic.esm.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@types/uuid": "^10.0.0",
|
|
40
40
|
"@typescript-eslint/eslint-plugin": "^8.23.0",
|
|
41
41
|
"@typescript-eslint/parser": "^8.23.0",
|
|
42
|
-
"esbuild": "^0.
|
|
42
|
+
"esbuild": "^0.25.0",
|
|
43
43
|
"esbuild-jest": "^0.5.0",
|
|
44
44
|
"eslint": "^8.57.1",
|
|
45
45
|
"jest": "^29.7.0",
|