@growthbook/edge-utils 0.0.1

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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.d.ts +3 -0
  3. package/dist/app.js +171 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/attributes.d.ts +5 -0
  6. package/dist/attributes.js +76 -0
  7. package/dist/attributes.js.map +1 -0
  8. package/dist/config.d.ts +33 -0
  9. package/dist/config.js +91 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/domMutations.d.ts +6 -0
  12. package/dist/domMutations.js +151 -0
  13. package/dist/domMutations.js.map +1 -0
  14. package/dist/generated/sdkWrapper.d.ts +1 -0
  15. package/dist/generated/sdkWrapper.js +5 -0
  16. package/dist/generated/sdkWrapper.js.map +1 -0
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.js +16 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/inject.d.ts +16 -0
  21. package/dist/inject.js +136 -0
  22. package/dist/inject.js.map +1 -0
  23. package/dist/redirect.d.ts +11 -0
  24. package/dist/redirect.js +43 -0
  25. package/dist/redirect.js.map +1 -0
  26. package/dist/routing.d.ts +2 -0
  27. package/dist/routing.js +47 -0
  28. package/dist/routing.js.map +1 -0
  29. package/dist/stickyBucketService.d.ts +14 -0
  30. package/dist/stickyBucketService.js +49 -0
  31. package/dist/stickyBucketService.js.map +1 -0
  32. package/dist/types.d.ts +60 -0
  33. package/dist/types.js +4 -0
  34. package/dist/types.js.map +1 -0
  35. package/package.json +31 -0
  36. package/scripts/generate-sdk-wrapper.js +22 -0
  37. package/src/app.ts +210 -0
  38. package/src/attributes.ts +97 -0
  39. package/src/config.ts +166 -0
  40. package/src/domMutations.ts +157 -0
  41. package/src/generated/sdkWrapper.ts +2 -0
  42. package/src/index.ts +13 -0
  43. package/src/inject.ts +230 -0
  44. package/src/redirect.ts +53 -0
  45. package/src/routing.ts +44 -0
  46. package/src/stickyBucketService.ts +48 -0
  47. package/src/types.ts +98 -0
  48. package/tsconfig.json +27 -0
@@ -0,0 +1,157 @@
1
+ import { AutoExperimentVariation, DOMMutation } from "@growthbook/growthbook";
2
+ import { parse } from "node-html-parser";
3
+
4
+ export async function applyDomMutations({
5
+ body,
6
+ nonce,
7
+ domChanges,
8
+ }: {
9
+ body: string;
10
+ nonce?: string;
11
+ domChanges: AutoExperimentVariation[];
12
+ }) {
13
+ if (!domChanges.length) return body;
14
+
15
+ const root = parse(body);
16
+ const headEl = root.querySelector("head");
17
+
18
+ domChanges.forEach(({ domMutations, css, js }) => {
19
+ if (css) {
20
+ const parentEl = headEl || root;
21
+ const el = parse(`<style>${css}</style>`);
22
+ parentEl.appendChild(el);
23
+ }
24
+ if (js) {
25
+ const parentEl = headEl || root;
26
+ const el = parse(
27
+ `<script>${js}</script>`,
28
+ ) as unknown as HTMLScriptElement;
29
+ if (nonce) {
30
+ el.nonce = nonce;
31
+ }
32
+ // @ts-ignore
33
+ parentEl.appendChild(el);
34
+ }
35
+
36
+ domMutations?.forEach((mutation: DOMMutation) => {
37
+ const {
38
+ attribute: attr,
39
+ action,
40
+ selector,
41
+ value,
42
+ parentSelector,
43
+ insertBeforeSelector,
44
+ } = mutation;
45
+
46
+ if (attr === "html") {
47
+ if (action === "append") {
48
+ return html(selector, (val) => val + (value ?? ""));
49
+ } else if (action === "set") {
50
+ return html(selector, () => value ?? "");
51
+ }
52
+ } else if (attr === "class") {
53
+ if (action === "append") {
54
+ return classes(selector, (val) => {
55
+ if (value) val.add(value);
56
+ });
57
+ } else if (action === "remove") {
58
+ return classes(selector, (val) => {
59
+ if (value) val.delete(value);
60
+ });
61
+ } else if (action === "set") {
62
+ return classes(selector, (val) => {
63
+ val.clear();
64
+ if (value) val.add(value);
65
+ });
66
+ }
67
+ } else if (attr === "position") {
68
+ if (action === "set" && parentSelector) {
69
+ return position(selector, () => ({
70
+ insertBeforeSelector,
71
+ parentSelector,
72
+ }));
73
+ }
74
+ } else {
75
+ if (action === "append") {
76
+ return attribute(selector, attr, (val) =>
77
+ val !== null ? val + (value ?? "") : value ?? "",
78
+ );
79
+ } else if (action === "set") {
80
+ return attribute(selector, attr, () => value ?? "");
81
+ } else if (action === "remove") {
82
+ return attribute(selector, attr, () => null);
83
+ }
84
+ }
85
+ });
86
+ });
87
+
88
+ body = root.toString();
89
+ return body;
90
+
91
+ function html(selector: string, cb: (val: string) => string) {
92
+ const els = root.querySelectorAll(selector);
93
+ els.map((el) => {
94
+ el.innerHTML = cb(el.innerHTML);
95
+ });
96
+ }
97
+
98
+ function classes(selector: string, cb: (val: Set<string>) => void) {
99
+ const els = root.querySelectorAll(selector);
100
+ els.map((el) => {
101
+ const classList = new Set(el.classNames);
102
+ cb(classList);
103
+ el.setAttribute("class", Array.from(classList).join(" "));
104
+ });
105
+ }
106
+
107
+ function attribute(
108
+ selector: string,
109
+ attr: string,
110
+ cb: (val: string | null) => string | null,
111
+ ) {
112
+ const validAttributeName = /^[a-zA-Z:_][a-zA-Z0-9:_.-]*$/;
113
+ if (!validAttributeName.test(attr)) {
114
+ return;
115
+ }
116
+ if (attr === "class" || attr === "className") {
117
+ return classes(selector, (classnames) => {
118
+ const mutatedClassnames = cb(Array.from(classnames).join(" "));
119
+ classnames.clear();
120
+ if (!mutatedClassnames) return;
121
+ mutatedClassnames
122
+ .split(/\s+/g)
123
+ .filter(Boolean)
124
+ .forEach((c) => classnames.add(c));
125
+ });
126
+ }
127
+ const els = root.querySelectorAll(selector);
128
+ els.map((el) => {
129
+ const val = cb(el.getAttribute(attr) || "");
130
+ if (val === null) {
131
+ el.removeAttribute(attr);
132
+ } else {
133
+ el.setAttribute(attr, val);
134
+ }
135
+ });
136
+ }
137
+
138
+ function position(
139
+ selector: string,
140
+ cb: () => { insertBeforeSelector?: string; parentSelector: string },
141
+ ) {
142
+ const els = root.querySelectorAll(selector);
143
+ els.map((el) => {
144
+ const { insertBeforeSelector, parentSelector } = cb();
145
+ const parent = root.querySelector(parentSelector);
146
+ const insertBefore = insertBeforeSelector
147
+ ? parent?.querySelector(insertBeforeSelector)
148
+ : null;
149
+ if (parent && insertBefore) {
150
+ insertBefore.insertAdjacentHTML("beforebegin", el.toString());
151
+ el.remove();
152
+ } else if (parent) {
153
+ parent.appendChild(el);
154
+ }
155
+ });
156
+ }
157
+ }
@@ -0,0 +1,2 @@
1
+
2
+ export const sdkWrapper = "var _growthbook=function(){\"use strict\";function t(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var i in n)t[i]=n[i]}return t}var e=function e(n,i){function r(e,r,s){if(\"undefined\"!=typeof document){\"number\"==typeof(s=t({},i,s)).expires&&(s.expires=new Date(Date.now()+864e5*s.expires)),s.expires&&(s.expires=s.expires.toUTCString()),e=encodeURIComponent(e).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var o=\"\";for(var u in s)s[u]&&(o+=\"; \"+u,!0!==s[u]&&(o+=\"=\"+s[u].split(\";\")[0]));return document.cookie=e+\"=\"+n.write(r,e)+o}}return Object.create({set:r,get:function(t){if(\"undefined\"!=typeof document&&(!arguments.length||t)){for(var e=document.cookie?document.cookie.split(\"; \"):[],i={},r=0;r<e.length;r++){var s=e[r].split(\"=\"),o=s.slice(1).join(\"=\");try{var u=decodeURIComponent(s[0]);if(i[u]=n.read(o,u),t===u)break}catch(t){}}return t?i[t]:i}},remove:function(e,n){r(e,\"\",t({},n,{expires:-1}))},withAttributes:function(n){return e(this.converter,t({},this.attributes,n))},withConverter:function(n){return e(t({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(i)},converter:{value:Object.freeze(n)}})}({read:function(t){return\'\"\'===t[0]&&(t=t.slice(1,-1)),t.replace(/(%[\\dA-F]{2})+/gi,decodeURIComponent)},write:function(t){return encodeURIComponent(t).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:\"/\"}),n=/^[a-zA-Z:_][a-zA-Z0-9:_.-]*$/,i={revert:function(){}},r=new Map,s=new Set;function o(t){var e=r.get(t);return e||r.set(t,e={element:t,attributes:{}}),e}function u(t,e,n,i,r){var s=n(t),o={isDirty:!1,originalValue:s,virtualValue:s,mutations:[],el:t,t:null,observer:new MutationObserver((function(){if(\"position\"!==e||!o.t){\"position\"===e&&(o.t=setTimeout((function(){o.t=null}),1e3));var i=n(t);\"position\"===e&&i.parentNode===o.virtualValue.parentNode&&i.insertBeforeNode===o.virtualValue.insertBeforeNode||i!==o.virtualValue&&(o.originalValue=i,r(o))}})),mutationRunner:r,setValue:i,getCurrentValue:n};return\"position\"===e&&t.parentNode?o.observer.observe(t.parentNode,{childList:!0,subtree:!0,attributes:!1,characterData:!1}):o.observer.observe(t,function(t){return\"html\"===t?{childList:!0,subtree:!0,attributes:!0,characterData:!0}:{childList:!1,subtree:!1,attributes:!0,attributeFilter:[t]}}(e)),o}function c(t,e){var n=e.getCurrentValue(e.el);e.virtualValue=t,t&&\"string\"!=typeof t?n&&t.parentNode===n.parentNode&&t.insertBeforeNode===n.insertBeforeNode||(e.isDirty=!0,C()):t!==n&&(e.isDirty=!0,C())}function a(t){var e=t.originalValue;t.mutations.forEach((function(t){return e=t.mutate(e)})),c(function(t){return g||(g=document.createElement(\"div\")),g.innerHTML=t,g.innerHTML}(e),t)}function h(t){var e=new Set(t.originalValue.split(/\\s+/).filter(Boolean));t.mutations.forEach((function(t){return t.mutate(e)})),c(Array.from(e).filter(Boolean).join(\" \"),t)}function l(t){var e=t.originalValue;t.mutations.forEach((function(t){return e=t.mutate(e)})),c(e,t)}function f(t){var e=t.originalValue;t.mutations.forEach((function(t){var n=function(t){var e=t.insertBeforeSelector,n=document.querySelector(t.parentSelector);if(!n)return null;var i=e?document.querySelector(e):null;return e&&!i?null:{parentNode:n,insertBeforeNode:i}}(t.mutate());e=n||e})),c(e,t)}var d=function(t){return t.innerHTML},w=function(t,e){return t.innerHTML=e};function y(t){var e=o(t);return e.html||(e.html=u(t,\"html\",d,w,a)),e.html}var p=function(t){return{parentNode:t.parentElement,insertBeforeNode:t.nextElementSibling}},v=function(t,e){e.insertBeforeNode&&!e.parentNode.contains(e.insertBeforeNode)||e.parentNode.insertBefore(t,e.insertBeforeNode)};function m(t){var e=o(t);return e.position||(e.position=u(t,\"position\",p,v,f)),e.position}var g,b,k=function(t,e){return e?t.className=e:t.removeAttribute(\"class\")},S=function(t){return t.className};function A(t){var e=o(t);return e.classes||(e.classes=u(t,\"class\",S,k,h)),e.classes}function _(t,e){var n,i=o(t);return i.attributes[e]||(i.attributes[e]=u(t,e,(n=e,function(t){var e;return null!=(e=t.getAttribute(n))?e:null}),function(t){return function(e,n){return null!==n?e.setAttribute(t,n):e.removeAttribute(t)}}(e),l)),i.attributes[e]}function x(t,e,n){if(n.isDirty){n.isDirty=!1;var i=n.virtualValue;n.mutations.length||function(t,e){var n,i,s=r.get(t);if(s)if(\"html\"===e)null==(n=s.html)||null==(i=n.observer)||i.disconnect(),delete s.html;else if(\"class\"===e){var o,u;null==(o=s.classes)||null==(u=o.observer)||u.disconnect(),delete s.classes}else if(\"position\"===e){var c,a;null==(c=s.position)||null==(a=c.observer)||a.disconnect(),delete s.position}else{var h,l,f;null==(h=s.attributes)||null==(l=h[e])||null==(f=l.observer)||f.disconnect(),delete s.attributes[e]}}(t,e),n.setValue(t,i)}}function E(t,e){t.html&&x(e,\"html\",t.html),t.classes&&x(e,\"class\",t.classes),t.position&&x(e,\"position\",t.position),Object.keys(t.attributes).forEach((function(n){x(e,n,t.attributes[n])}))}function C(){r.forEach(E)}function O(t){if(\"position\"!==t.kind||1!==t.elements.size){var e=new Set(t.elements);document.querySelectorAll(t.selector).forEach((function(n){e.has(n)||(t.elements.add(n),function(t,e){var n=null;\"html\"===t.kind?n=y(e):\"class\"===t.kind?n=A(e):\"attribute\"===t.kind?n=_(e,t.attribute):\"position\"===t.kind&&(n=m(e)),n&&(n.mutations.push(t),n.mutationRunner(n))}(t,n))}))}}function $(){s.forEach(O)}function R(t){return\"undefined\"==typeof document?i:(s.add(t),O(t),{revert:function(){var e;(e=t).elements.forEach((function(t){return function(t,e){var n=null;if(\"html\"===t.kind?n=y(e):\"class\"===t.kind?n=A(e):\"attribute\"===t.kind?n=_(e,t.attribute):\"position\"===t.kind&&(n=m(e)),n){var i=n.mutations.indexOf(t);-1!==i&&n.mutations.splice(i,1),n.mutationRunner(n)}}(e,t)})),e.elements.clear(),s.delete(e)}})}function B(t,e){return R({kind:\"html\",elements:new Set,mutate:e,selector:t})}function F(t,e){return R({kind:\"class\",elements:new Set,mutate:e,selector:t})}function T(t,e,r){return n.test(e)?\"class\"===e||\"className\"===e?F(t,(function(t){var e=r(Array.from(t).join(\" \"));t.clear(),e&&e.split(/\\s+/g).filter(Boolean).forEach((function(e){return t.add(e)}))})):R({kind:\"attribute\",attribute:e,elements:new Set,mutate:r,selector:t}):i}\"undefined\"!=typeof document&&(b||(b=new MutationObserver((function(){$()}))),$(),b.observe(document.documentElement,{childList:!0,subtree:!0,attributes:!1,characterData:!1}));const N={fetch:globalThis.fetch?globalThis.fetch.bind(globalThis):void 0,SubtleCrypto:globalThis.crypto?globalThis.crypto.subtle:void 0,EventSource:globalThis.EventSource};function M(t){let e=2166136261;const n=t.length;for(let i=0;i<n;i++)e^=t.charCodeAt(i),e+=(e<<1)+(e<<4)+(e<<7)+(e<<8)+(e<<24);return e>>>0}function U(t,e,n){return 2===n?M(M(t+e)+\"\")%1e4/1e4:1===n?M(e+t)%1e3/1e3:null}function V(t,e){return t>=e[0]&&t<e[1]}function I(t){try{const e=t.replace(/([^\\\\])\\//g,\"$1\\\\/\");return new RegExp(e)}catch(t){return void console.error(t)}}function D(t,e){if(!e.length)return!1;let n=!1,i=!1;for(let r=0;r<e.length;r++){const s=J(t,e[r].type,e[r].pattern);if(!1===e[r].include){if(s)return!1}else n=!0,s&&(i=!0)}return i||!n}function J(t,e,n){try{const i=new URL(t,\"https://_\");if(\"regex\"===e){const t=I(n);return!!t&&(t.test(i.href)||t.test(i.href.substring(i.origin.length)))}return\"simple\"===e&&function(t,e){try{const n=new URL(e.replace(/^([^:/?]*)\\./i,\"https://$1.\").replace(/\\*/g,\"_____\"),\"https://_____\"),i=[[t.host,n.host,!1],[t.pathname,n.pathname,!0]];return n.hash&&i.push([t.hash,n.hash,!1]),n.searchParams.forEach(((e,n)=>{i.push([t.searchParams.get(n)||\"\",e,!1])})),!i.some((t=>!function(t,e,n){try{let i=e.replace(/[*.+?^${}()|[\\]\\\\]/g,\"\\\\$&\").replace(/_____/g,\".*\");return n&&(i=\"\\\\/?\"+i.replace(/(^\\/|\\/$)/g,\"\")+\"\\\\/?\"),new RegExp(\"^\"+i+\"$\",\"i\").test(t)}catch(t){return!1}}(t[0],t[1],t[2])))}catch(t){return!1}}(i,n)}catch(t){return!1}}const j=t=>Uint8Array.from(atob(t),(t=>t.charCodeAt(0)));async function K(t,e,n){if(e=e||\"\",!(n=n||globalThis.crypto&&globalThis.crypto.subtle||N.SubtleCrypto))throw new Error(\"No SubtleCrypto implementation found\");try{const i=await n.importKey(\"raw\",j(e),{name:\"AES-CBC\",length:128},!0,[\"encrypt\",\"decrypt\"]),[r,s]=t.split(\".\"),o=await n.decrypt({name:\"AES-CBC\",iv:j(r)},i,j(s));return(new TextDecoder).decode(o)}catch(t){throw new Error(\"Failed to decrypt\")}}function L(t){return\"string\"==typeof t?t:JSON.stringify(t)}function H(t){\"number\"==typeof t&&(t+=\"\"),t&&\"string\"==typeof t||(t=\"0\");const e=t.replace(/(^v|\\+.*$)/g,\"\").split(/[-.]/);return 3===e.length&&e.push(\"~\"),e.map((t=>t.match(/^[0-9]+$/)?t.padStart(5,\" \"):t)).join(\"-\")}function q(t){return\"object\"==typeof t&&null!==t}function P(t){return t.urlPatterns&&t.variations.some((t=>q(t)&&\"urlRedirect\"in t))?\"redirect\":t.variations.some((t=>q(t)&&(t.domMutations||\"js\"in t||\"css\"in t)))?\"visual\":\"unknown\"}const z={};function G(t,e){if(\"$or\"in e)return tt(t,e.$or);if(\"$nor\"in e)return!tt(t,e.$nor);if(\"$and\"in e)return function(t,e){for(let n=0;n<e.length;n++)if(!G(t,e[n]))return!1;return!0}(t,e.$and);if(\"$not\"in e)return!G(t,e.$not);for(const[n,i]of Object.entries(e))if(!Q(i,Z(t,n)))return!1;return!0}function Z(t,e){const n=e.split(\".\");let i=t;for(let t=0;t<n.length;t++){if(!i||\"object\"!=typeof i||!(n[t]in i))return null;i=i[n[t]]}return i}function Q(t,e){if(\"string\"==typeof t)return e+\"\"===t;if(\"number\"==typeof t)return 1*e===t;if(\"boolean\"==typeof t)return!!e===t;if(null===t)return null===e;if(Array.isArray(t)||!W(t))return JSON.stringify(e)===JSON.stringify(t);for(const n in t)if(!Y(n,e,t[n]))return!1;return!0}function W(t){const e=Object.keys(t);return e.length>0&&e.filter((t=>\"$\"===t[0])).length===e.length}function X(t,e){return Array.isArray(t)?t.some((t=>e.includes(t))):e.includes(t)}function Y(t,e,n){switch(t){case\"$veq\":return H(e)===H(n);case\"$vne\":return H(e)!==H(n);case\"$vgt\":return H(e)>H(n);case\"$vgte\":return H(e)>=H(n);case\"$vlt\":return H(e)<H(n);case\"$vlte\":return H(e)<=H(n);case\"$eq\":return e===n;case\"$ne\":return e!==n;case\"$lt\":return e<n;case\"$lte\":return e<=n;case\"$gt\":return e>n;case\"$gte\":return e>=n;case\"$exists\":return n?null!=e:null==e;case\"$in\":return!!Array.isArray(n)&&X(e,n);case\"$nin\":return!!Array.isArray(n)&&!X(e,n);case\"$not\":return!Q(n,e);case\"$size\":return!!Array.isArray(e)&&Q(n,e.length);case\"$elemMatch\":return function(t,e){if(!Array.isArray(t))return!1;const n=W(e)?t=>Q(e,t):t=>G(t,e);for(let e=0;e<t.length;e++)if(t[e]&&n(t[e]))return!0;return!1}(e,n);case\"$all\":if(!Array.isArray(e))return!1;for(let t=0;t<n.length;t++){let i=!1;for(let r=0;r<e.length;r++)if(Q(n[t],e[r])){i=!0;break}if(!i)return!1}return!0;case\"$regex\":try{return(i=n,z[i]||(z[i]=new RegExp(i.replace(/([^\\\\])\\//g,\"$1\\\\/\"))),z[i]).test(e)}catch(t){return!1}case\"$type\":return function(t){if(null===t)return\"null\";if(Array.isArray(t))return\"array\";const e=typeof t;return[\"string\",\"number\",\"boolean\",\"object\",\"undefined\"].includes(e)?e:\"unknown\"}(e)===n;default:return console.error(\"Unknown operator: \"+t),!1}var i}function tt(t,e){if(!e.length)return!0;for(let n=0;n<e.length;n++)if(G(t,e[n]))return!0;return!1}const et=\"undefined\"!=typeof window&&\"undefined\"!=typeof document,nt=function(){let t;try{t=\"1.0.0\"}catch(e){t=\"\"}return t}(),it={staleTTL:6e4,maxAge:864e5,cacheKey:\"gbFeaturesCache\",backgroundSync:!0,maxEntries:10,disableIdleStreams:!1,idleStreamInterval:2e4,disableCache:!1},rt=N,st={fetchFeaturesCall:t=>{let{host:e,clientKey:n,headers:i}=t;return rt.fetch(\"\".concat(e,\"/api/features/\").concat(n),{headers:i})},fetchRemoteEvalCall:t=>{let{host:e,clientKey:n,payload:i,headers:r}=t;const s={method:\"POST\",headers:{\"Content-Type\":\"application/json\",...r},body:JSON.stringify(i)};return rt.fetch(\"\".concat(e,\"/api/eval/\").concat(n),s)},eventSourceCall:t=>{let{host:e,clientKey:n,headers:i}=t;return i?new rt.EventSource(\"\".concat(e,\"/sub/\").concat(n),{headers:i}):new rt.EventSource(\"\".concat(e,\"/sub/\").concat(n))},startIdleListener:()=>{let t;if(\"undefined\"==typeof window||\"undefined\"==typeof document)return;const e=()=>{\"visible\"===document.visibilityState?(window.clearTimeout(t),ht.forEach((t=>{t&&\"idle\"===t.state&&At(t)}))):\"hidden\"===document.visibilityState&&(t=window.setTimeout(dt,it.idleStreamInterval))};return document.addEventListener(\"visibilitychange\",e),()=>document.removeEventListener(\"visibilitychange\",e)},stopIdleListener:()=>{}};try{globalThis.localStorage&&(rt.localStorage=globalThis.localStorage)}catch(t){}const ot=new Map;let ut=!1;const ct=new Map,at=new Map,ht=new Map,lt=new Set;function ft(t){const e=yt(t),n=ot.get(e)||new Set;n.add(t),ot.set(e,n)}function dt(){ht.forEach((t=>{t&&(t.state=\"idle\",St(t))}))}async function wt(){try{if(!rt.localStorage)return;await rt.localStorage.setItem(it.cacheKey,JSON.stringify(Array.from(ct.entries())))}catch(t){}}function yt(t){const[e,n]=t.getApiInfo();return\"\".concat(e,\"||\").concat(n)}function pt(t){const e=yt(t);if(!t.isRemoteEval())return e;const n=t.getAttributes(),i=t.getCacheKeyAttributes()||Object.keys(t.getAttributes()),r={};i.forEach((t=>{r[t]=n[t]}));const s=t.getForcedVariations(),o=t.getUrl();return\"\".concat(e,\"||\").concat(JSON.stringify({ca:r,fv:s,url:o}))}function vt(){const t=Array.from(ct.entries()).map((t=>{let[e,n]=t;return{key:e,staleAt:n.staleAt.getTime()}})).sort(((t,e)=>t.staleAt-e.staleAt)),e=Math.min(Math.max(0,ct.size-it.maxEntries),ct.size);for(let n=0;n<e;n++)ct.delete(t[n].key)}function mt(t,e,n){const i=n.dateUpdated||\"\",r=new Date(Date.now()+it.staleTTL),s=ct.get(e);if(s&&i&&s.version===i)return s.staleAt=r,void wt();ct.set(e,{data:n,version:i,staleAt:r,sse:lt.has(t)}),vt(),wt();const o=ot.get(t);o&&o.forEach((t=>async function(t,e){await t.setPayload(e||t.getPayload())}(t,n)))}async function gt(t){const{apiHost:e,apiRequestHeaders:n}=t.getApiHosts(),i=t.getClientKey(),r=t.isRemoteEval(),s=yt(t),o=pt(t);let u=at.get(o);return u||(u=(r?st.fetchRemoteEvalCall({host:e,clientKey:i,payload:{attributes:t.getAttributes(),forcedVariations:t.getForcedVariations(),forcedFeatures:Array.from(t.getForcedFeatures().entries()),url:t.getUrl()},headers:n}):st.fetchFeaturesCall({host:e,clientKey:i,headers:n})).then((t=>{if(!t.ok)throw new Error(\"HTTP error: \".concat(t.status));return\"enabled\"===t.headers.get(\"x-sse-support\")&&lt.add(s),t.json()})).then((e=>(mt(s,o,e),bt(t),at.delete(o),{data:e,success:!0,source:\"network\"}))).catch((t=>(at.delete(o),{data:null,source:\"error\",success:!1,error:t}))),at.set(o,u)),u}function bt(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];const n=yt(t),i=pt(t),{streamingHost:r,streamingHostRequestHeaders:s}=t.getApiHosts(),o=t.getClientKey();if(e&&lt.add(n),it.backgroundSync&&lt.has(n)&&rt.EventSource){if(ht.has(n))return;const t={src:null,host:r,clientKey:o,headers:s,cb:e=>{try{if(\"features-updated\"===e.type){const t=ot.get(n);t&&t.forEach((t=>{gt(t)}))}else if(\"features\"===e.type){const t=JSON.parse(e.data);mt(n,i,t)}t.errors=0}catch(e){kt(t)}},errors:0,state:\"active\"};ht.set(n,t),At(t)}}function kt(t){if(\"idle\"!==t.state&&(t.errors++,t.errors>3||t.src&&2===t.src.readyState)){const e=Math.pow(3,t.errors-3)*(1e3+1e3*Math.random());St(t),setTimeout((()=>{[\"idle\",\"active\"].includes(t.state)||At(t)}),Math.min(e,3e5))}}function St(t){t.src&&(t.src.onopen=null,t.src.onerror=null,t.src.close(),t.src=null,\"active\"===t.state&&(t.state=\"disabled\"))}function At(t){t.src=st.eventSourceCall({host:t.host,clientKey:t.clientKey,headers:t.headers}),t.state=\"active\",t.src.addEventListener(\"features\",t.cb),t.src.addEventListener(\"features-updated\",t.cb),t.src.onerror=()=>kt(t),t.src.onopen=()=>{t.errors=0}}class _t{async getAllAssignments(t){const e={};return(await Promise.all(Object.entries(t).map((t=>{let[e,n]=t;return this.getAssignments(e,n)})))).forEach((t=>{if(t){const n=\"\".concat(t.attributeName,\"||\").concat(t.attributeValue);e[n]=t}})),e}}window.dataLayer=window.dataLayer||[];const xt=document.currentScript,Et=xt?xt.dataset:{},Ct=window.growthbook_config||{};function Ot(t){const e=(\"; \"+document.cookie).split(\"; \".concat(t,\"=\"));return 2===e.length?e[1].split(\";\")[0]:\"\"}function $t(){return window.crypto&&crypto.randomUUID?crypto.randomUUID():\"10000000-1000-4000-8000-100000000000\".replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)))}const Rt=Ct.uuidCookieName||Et.uuidCookieName||\"gbuuid\",Bt=Ct.uuidKey||Et.uuidKey||\"id\";let Ft,Tt=Ct.uuid||Et.uuid||\"\";function Nt(){!function(t,e){const n=new Date;n.setTime(n.getTime()+3456e7),document.cookie=t+\"=\"+e+\";path=/;expires=\"+n.toUTCString()}(Rt,Tt)}function Mt(){let t={};try{const e=sessionStorage.getItem(\"utm_params\");if(e&&(t=JSON.parse(e)),location.search){const e=new URLSearchParams(location.search);let n=!1;[\"source\",\"medium\",\"campaign\",\"term\",\"content\"].forEach((i=>{const r=\"utm_\".concat(i),s=\"utm\"+i[0].toUpperCase()+i.slice(1);e.has(r)&&(t[s]=e.get(r)||\"\",n=!0)})),n&&sessionStorage.setItem(\"utm_params\",JSON.stringify(t))}}catch(t){}return t}function Ut(){if(!window.dataLayer||!window.dataLayer.forEach)return{};const t={};return window.dataLayer.forEach((e=>{e&&\"object\"==typeof e&&!(\"length\"in e)&&(\"event\"in e||Object.keys(e).forEach((n=>{if(\"string\"!=typeof n||n.match(/^(gtm)/))return;const i=e[n];[\"string\",\"number\",\"boolean\"].includes(typeof i)&&(t[n]=i)})))})),t}function Vt(){const t=Et.noAutoAttributes?{}:function(t,e){const n=null==t.noAutoCookies,i=navigator.userAgent,r=i.match(/Edg/)?\"edge\":i.match(/Chrome/)?\"chrome\":i.match(/Firefox/)?\"firefox\":i.match(/Safari/)?\"safari\":\"unknown\",s=function(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];return Tt||(Tt=Ot(Rt),Tt||(Tt=$t(),t&&Nt(),Tt))}(n);return(e.persistUuidOnLoad||t.persistUuidOnLoad)&&n&&Nt(),{...Ut(),[Bt]:s,url:location.href,path:location.pathname,host:location.host,query:location.search,pageTitle:document&&document.title,deviceType:i.match(/Mobi/)?\"mobile\":\"desktop\",browser:r,...Mt()}}(Et,Ct);return Ct.attributes&&Object.assign(t,Ct.attributes),t}\"cookie\"===Ct.useStickyBucketService||\"cookie\"===Et.useStickyBucketService?Ft=new class extends _t{constructor(t){let{prefix:e=\"gbStickyBuckets__\",jsCookie:n,cookieAttributes:i={}}=t;super(),this.prefix=e,this.jsCookie=n,this.cookieAttributes=i}async getAssignments(t,e){const n=\"\".concat(t,\"||\").concat(e);let i=null;if(!this.jsCookie)return i;try{const t=this.jsCookie.get(this.prefix+n),e=JSON.parse(t||\"{}\");e.attributeName&&e.attributeValue&&e.assignments&&(i=e)}catch(t){}return i}async saveAssignments(t){const e=\"\".concat(t.attributeName,\"||\").concat(t.attributeValue);if(!this.jsCookie)return;const n=JSON.stringify(t);this.jsCookie.set(this.prefix+e,n,this.cookieAttributes)}}({prefix:Ct.stickyBucketPrefix||Et.stickyBucketPrefix||void 0,jsCookie:e}):\"localStorage\"!==Ct.useStickyBucketService&&\"localStorage\"!==Et.useStickyBucketService||(Ft=new class extends _t{constructor(t){t=t||{},super(),this.prefix=t.prefix||\"gbStickyBuckets__\";try{this.localStorage=t.localStorage||globalThis.localStorage}catch(t){}}async getAssignments(t,e){const n=\"\".concat(t,\"||\").concat(e);let i=null;if(!this.localStorage)return i;try{const t=await this.localStorage.getItem(this.prefix+n)||\"{}\",e=JSON.parse(t);e.attributeName&&e.attributeValue&&e.assignments&&(i=e)}catch(t){}return i}async saveAssignments(t){const e=\"\".concat(t.attributeName,\"||\").concat(t.attributeValue);if(this.localStorage)try{await this.localStorage.setItem(this.prefix+e,JSON.stringify(t))}catch(t){}}}({prefix:Ct.stickyBucketPrefix||Et.stickyBucketPrefix||void 0}));const It=new class{constructor(t){if(t=t||{},this.version=nt,this.i=this.context=t,this.o=t.renderer||null,this.u=new Set,this.h=new Set,this.l={},this.debug=!!t.debug,this.p=new Set,this.v=[],this.m=0,this.ready=!1,this.g=new Map,this.k=new Map,this.S={},this.A=new Map,this._=new Set,this.C=!1,this.O=\"\",this.$=new Map,this.R=!t.disableExperimentsOnLoad,t.remoteEval){if(t.decryptionKey)throw new Error(\"Encryption is not available for remoteEval\");if(!t.clientKey)throw new Error(\"Missing clientKey\");let e=!1;try{e=!!new URL(t.apiHost||\"\").hostname.match(/growthbook\\.io$/i)}catch(t){}if(e)throw new Error(\"Cannot use remoteEval on GrowthBook Cloud\")}else if(t.cacheKeyAttributes)throw new Error(\"cacheKeyAttributes are only used for remoteEval\");if(t.features&&(this.ready=!0),et&&t.enableDevMode&&(window._growthbook=this,document.dispatchEvent(new Event(\"gbloaded\"))),t.experiments?(this.ready=!0,this.B()):t.antiFlicker&&this.F(),this.i.stickyBucketService&&this.i.stickyBucketAssignmentDocs)for(const t in this.i.stickyBucketAssignmentDocs){const e=this.i.stickyBucketAssignmentDocs[t];e&&this.i.stickyBucketService.saveAssignments(e).catch((()=>{}))}this.ready&&this.refreshStickyBuckets(this.getPayload())}async setPayload(t){this.T=t;const e=await this.decryptPayload(t);this.N=e,await this.refreshStickyBuckets(e),e.features&&(this.i.features=e.features),e.experiments&&(this.i.experiments=e.experiments,this.B()),this.ready=!0,this.M()}initSync(t){this.C=!0;const e=t.payload;if(e.encryptedExperiments||e.encryptedFeatures)throw new Error(\"initSync does not support encrypted payloads\");if(this.i.stickyBucketService&&!this.i.stickyBucketAssignmentDocs)throw new Error(\"initSync requires you to pass stickyBucketAssignmentDocs into the GrowthBook constructor\");if(this.T=e,this.N=e,e.features&&(this.i.features=e.features),e.experiments&&(this.i.experiments=e.experiments,this.B()),this.ready=!0,t.streaming){if(!this.i.clientKey)throw new Error(\"Must specify clientKey to enable streaming\");bt(this,!0),ft(this)}return this}async init(t){if(this.C=!0,(t=t||{}).payload){if(await this.setPayload(t.payload),t.streaming){if(!this.i.clientKey)throw new Error(\"Must specify clientKey to enable streaming\");bt(this,!0),ft(this)}return{success:!0,source:\"init\"}}{const{data:e,...n}=await this.U({...t,allowStale:!0});return t.streaming&&ft(this),await this.setPayload(e||{}),n}}async loadFeatures(t){this.C=!0,(t=t||{}).autoRefresh&&(this.i.subscribeToChanges=!0);const{data:e}=await this.U({...t,allowStale:!0});await this.setPayload(e||{}),this.V()&&ft(this)}async refreshFeatures(t){const e=await this.U({...t||{},allowStale:!1});e.data&&await this.setPayload(e.data)}getApiInfo(){return[this.getApiHosts().apiHost,this.getClientKey()]}getApiHosts(){const t=this.i.apiHost||\"https://cdn.growthbook.io\";return{apiHost:t.replace(/\\/*$/,\"\"),streamingHost:(this.i.streamingHost||t).replace(/\\/*$/,\"\"),apiRequestHeaders:this.i.apiHostRequestHeaders,streamingHostRequestHeaders:this.i.streamingHostRequestHeaders}}getClientKey(){return this.i.clientKey||\"\"}getPayload(){return this.T||{features:this.getFeatures(),experiments:this.getExperiments()}}getDecryptedPayload(){return this.N||this.getPayload()}isRemoteEval(){return this.i.remoteEval||!1}getCacheKeyAttributes(){return this.i.cacheKeyAttributes}async U(t){var e;let{timeout:n,skipCache:i,allowStale:r,streaming:s}=t;if(!this.i.clientKey)throw new Error(\"Missing clientKey\");return async function(t){let{instance:e,timeout:n,skipCache:i,allowStale:r,backgroundSync:s}=t;return s||(it.backgroundSync=!1),async function(t){let{instance:e,allowStale:n,timeout:i,skipCache:r}=t;const s=yt(e),o=pt(e),u=new Date,c=new Date(u.getTime()-it.maxAge+it.staleTTL);await async function(){if(!ut){ut=!0;try{if(rt.localStorage){const t=await rt.localStorage.getItem(it.cacheKey);if(!it.disableCache&&t){const e=JSON.parse(t);e&&Array.isArray(e)&&e.forEach((t=>{let[e,n]=t;ct.set(e,{...n,staleAt:new Date(n.staleAt)})})),vt()}}}catch(t){}{const t=st.startIdleListener();t&&(st.stopIdleListener=t)}}}();const a=it.disableCache||r?void 0:ct.get(o);if(a&&(n||a.staleAt>u)&&a.staleAt>c)return a.sse&&lt.add(s),a.staleAt<u?gt(e):bt(e),{data:a.data,success:!0,source:\"cache\"};{const t=await function(t,e){return new Promise((n=>{let i,r=!1;const s=t=>{r||(r=!0,i&&clearTimeout(i),n(t||null))};e&&(i=setTimeout((()=>s()),e)),t.then((t=>s(t))).catch((()=>s()))}))}(gt(e),i);return t||{data:null,success:!1,source:\"timeout\",error:new Error(\"Timeout\")}}}({instance:e,allowStale:r,timeout:n,skipCache:i})}({instance:this,timeout:n,skipCache:i||this.i.disableCache,allowStale:r,backgroundSync:null===(e=null!=s?s:this.i.backgroundSync)||void 0===e||e})}M(){if(this.o)try{this.o()}catch(t){console.error(\"Failed to render\",t)}}setFeatures(t){this.i.features=t,this.ready=!0,this.M()}async setEncryptedFeatures(t,e,n){const i=await K(t,e||this.i.decryptionKey,n);this.setFeatures(JSON.parse(i))}setExperiments(t){this.i.experiments=t,this.ready=!0,this.B()}async setEncryptedExperiments(t,e,n){const i=await K(t,e||this.i.decryptionKey,n);this.setExperiments(JSON.parse(i))}async decryptPayload(t,e,n){return(t={...t}).encryptedFeatures&&(t.features=JSON.parse(await K(t.encryptedFeatures,e||this.i.decryptionKey,n)),delete t.encryptedFeatures),t.encryptedExperiments&&(t.experiments=JSON.parse(await K(t.encryptedExperiments,e||this.i.decryptionKey,n)),delete t.encryptedExperiments),t}async setAttributes(t){this.i.attributes=t,this.i.stickyBucketService&&await this.refreshStickyBuckets(),this.i.remoteEval?await this.I():(this.M(),this.B())}async updateAttributes(t){return this.setAttributes({...this.i.attributes,...t})}async setAttributeOverrides(t){this.S=t,this.i.stickyBucketService&&await this.refreshStickyBuckets(),this.i.remoteEval?await this.I():(this.M(),this.B())}async setForcedVariations(t){this.i.forcedVariations=t||{},this.i.remoteEval?await this.I():(this.M(),this.B())}setForcedFeatures(t){this.k=t,this.M()}async setURL(t){if(this.i.url=t,this.O=\"\",this.i.remoteEval)return await this.I(),void this.B(!0);this.B(!0)}getAttributes(){return{...this.i.attributes,...this.S}}getForcedVariations(){return this.i.forcedVariations||{}}getForcedFeatures(){return this.k||new Map}getStickyBucketAssignmentDocs(){return this.i.stickyBucketAssignmentDocs||{}}getUrl(){return this.i.url||\"\"}getFeatures(){return this.i.features||{}}getExperiments(){return this.i.experiments||[]}getCompletedChangeIds(){return Array.from(this.h)}subscribe(t){return this.p.add(t),()=>{this.p.delete(t)}}V(){var t;return(null===(t=this.i.backgroundSync)||void 0===t||t)&&this.i.subscribeToChanges}async I(){if(!this.i.remoteEval)return;if(!this.C)return;const t=await this.U({allowStale:!1});t.data&&await this.setPayload(t.data)}getAllResults(){return new Map(this.g)}destroy(){var t;this.p.clear(),this.g.clear(),this.u.clear(),this.h.clear(),this.$.clear(),this.l={},this.v=[],this.T=void 0,this.m&&clearTimeout(this.m),t=this,ot.forEach((e=>e.delete(t))),et&&window._growthbook===this&&delete window._growthbook,this.A.forEach((t=>{t.undo()})),this.A.clear(),this._.clear()}setRenderer(t){this.o=t}forceVariation(t,e){this.i.forcedVariations=this.i.forcedVariations||{},this.i.forcedVariations[t]=e,this.i.remoteEval?this.I():(this.B(),this.M())}run(t){const e=this.D(t,null);return this.J(t,e),e}triggerExperiment(t){return this._.add(t),this.i.experiments?this.i.experiments.filter((e=>e.key===t)).map((t=>this.j(t))).filter((t=>null!==t)):null}triggerAutoExperiments(){this.R=!0,this.B(!0)}j(t,e){const n=this.A.get(t);if(t.manual&&!this._.has(t.key)&&!n)return null;const i=this.K(t)?this.L(t,-1,!1,\"\"):this.run(t),r=JSON.stringify(i.value);if(!e&&i.inExperiment&&n&&n.valueHash===r)return i;if(n&&this.H(t),i.inExperiment){const e=P(t);if(\"redirect\"===e&&i.value.urlRedirect&&t.urlPatterns){const e=t.persistQueryString?function(t,e){let n,i;try{n=new URL(t),i=new URL(e)}catch(t){return console.error(\"Unable to merge query strings: \".concat(t)),e}return n.searchParams.forEach(((t,e)=>{i.searchParams.has(e)||i.searchParams.set(e,t)})),i.toString()}(this.q(),i.value.urlRedirect):i.value.urlRedirect;if(D(e,t.urlPatterns))return this.log(\"Skipping redirect because original URL matches redirect URL\",{id:t.key}),i;this.O=e;const n=this.P();var s;if(n)if(et)this.F(),window.setTimeout((()=>{try{n(e)}catch(t){console.error(t)}}),null!==(s=this.i.navigateDelay)&&void 0!==s?s:100);else try{n(e)}catch(t){console.error(t)}}else if(\"visual\"===e){const e=this.i.applyDomChangesCallback?this.i.applyDomChangesCallback(i.value):this.G(i.value);e&&this.A.set(t,{undo:e,valueHash:r})}}return i}H(t){const e=this.A.get(t);e&&(e.undo(),this.A.delete(t))}B(t){if(!this.R)return;const e=this.i.experiments||[],n=new Set(e);this.A.forEach(((t,e)=>{n.has(e)||(t.undo(),this.A.delete(e))}));for(const n of e){const e=this.j(n,t);if(null!=e&&e.inExperiment&&\"redirect\"===P(n))break}}J(t,e){const n=t.key,i=this.g.get(n);i&&i.result.inExperiment===e.inExperiment&&i.result.variationId===e.variationId||(this.g.set(n,{experiment:t,result:e}),this.p.forEach((n=>{try{n(t,e)}catch(t){console.error(t)}})))}Z(t,e){if(\"override\"===e.source)return;const n=JSON.stringify(e.value);if(this.l[t]!==n){if(this.l[t]=n,this.i.onFeatureUsage)try{this.i.onFeatureUsage(t,e)}catch(t){}et&&window.fetch&&(this.v.push({key:t,on:e.on}),this.m||(this.m=window.setTimeout((()=>{this.m=0;const t=[...this.v];this.v=[],this.i.realtimeKey&&window.fetch(\"https://rt.growthbook.io/?key=\".concat(this.i.realtimeKey,\"&events=\").concat(encodeURIComponent(JSON.stringify(t))),{cache:\"no-cache\",mode:\"no-cors\"}).catch((()=>{}))}),this.i.realtimeInterval||2e3)))}}W(t,e,n,i,r,s){const o={value:e,on:!!e,off:!e,source:n,ruleId:i||\"\"};return r&&(o.experiment=r),s&&(o.experimentResult=s),this.Z(t,o),o}isOn(t){return this.evalFeature(t).on}isOff(t){return this.evalFeature(t).off}getFeatureValue(t,e){const n=this.evalFeature(t).value;return null===n?e:n}feature(t){return this.evalFeature(t)}evalFeature(t){return this.X(t)}X(t,e){if((e=e||{evaluatedFeatures:new Set}).evaluatedFeatures.has(t))return this.W(t,null,\"cyclicPrerequisite\");if(e.evaluatedFeatures.add(t),e.id=t,this.k.has(t))return this.W(t,this.k.get(t),\"override\");if(!this.i.features||!this.i.features[t])return this.W(t,null,\"unknownFeature\");const n=this.i.features[t];if(n.rules)t:for(const i of n.rules){if(i.parentConditions)for(const n of i.parentConditions){const i=this.X(n.id,e);if(\"cyclicPrerequisite\"===i.source)return this.W(t,null,\"cyclicPrerequisite\");if(!G({value:i.value},n.condition||{})){if(n.gate)return this.W(t,null,\"prerequisite\");continue t}}if(i.filters&&this.Y(i.filters))continue;if(\"force\"in i){if(i.condition&&!this.tt(i.condition))continue;if(!this.et(i.seed||t,i.hashAttribute,this.i.stickyBucketService&&!i.disableStickyBucketing?i.fallbackAttribute:void 0,i.range,i.coverage,i.hashVersion))continue;return i.tracks&&i.tracks.forEach((t=>{this.nt(t.experiment,t.result)})),this.W(t,i.force,\"force\",i.id)}if(!i.variations)continue;const n={variations:i.variations,key:i.key||t};\"coverage\"in i&&(n.coverage=i.coverage),i.weights&&(n.weights=i.weights),i.hashAttribute&&(n.hashAttribute=i.hashAttribute),i.fallbackAttribute&&(n.fallbackAttribute=i.fallbackAttribute),i.disableStickyBucketing&&(n.disableStickyBucketing=i.disableStickyBucketing),void 0!==i.bucketVersion&&(n.bucketVersion=i.bucketVersion),void 0!==i.minBucketVersion&&(n.minBucketVersion=i.minBucketVersion),i.namespace&&(n.namespace=i.namespace),i.meta&&(n.meta=i.meta),i.ranges&&(n.ranges=i.ranges),i.name&&(n.name=i.name),i.phase&&(n.phase=i.phase),i.seed&&(n.seed=i.seed),i.hashVersion&&(n.hashVersion=i.hashVersion),i.filters&&(n.filters=i.filters),i.condition&&(n.condition=i.condition);const r=this.D(n,t);if(this.J(n,r),r.inExperiment&&!r.passthrough)return this.W(t,r.value,\"experiment\",i.id,n,r)}return this.W(t,void 0===n.defaultValue?null:n.defaultValue,\"defaultValue\")}et(t,e,n,i,r,s){if(!i&&void 0===r)return!0;if(!i&&0===r)return!1;const{hashValue:o}=this.it(e,n);if(!o)return!1;const u=U(t,o,s||1);return null!==u&&(i?V(u,i):void 0===r||u<=r)}tt(t){return G(this.getAttributes(),t)}Y(t){return t.some((t=>{const{hashValue:e}=this.it(t.attribute);if(!e)return!0;const n=U(t.seed,e,t.hashVersion||2);return null===n||!t.ranges.some((t=>V(n,t)))}))}D(t,e){const n=t.key,i=t.variations.length;if(i<2)return this.L(t,-1,!1,e);if(!1===this.i.enabled)return this.L(t,-1,!1,e);if((t=this.rt(t)).urlPatterns&&!D(this.q(),t.urlPatterns))return this.L(t,-1,!1,e);const r=function(t,e,n){if(!e)return null;const i=e.split(\"?\")[1];if(!i)return null;const r=i.replace(/#.*/,\"\").split(\"&\").map((t=>t.split(\"=\",2))).filter((e=>{let[n]=e;return n===t})).map((t=>{let[,e]=t;return parseInt(e)}));return r.length>0&&r[0]>=0&&r[0]<n?r[0]:null}(n,this.q(),i);if(null!==r)return this.L(t,r,!1,e);if(this.i.forcedVariations&&n in this.i.forcedVariations)return this.L(t,this.i.forcedVariations[n],!1,e);if(\"draft\"===t.status||!1===t.active)return this.L(t,-1,!1,e);const{hashAttribute:s,hashValue:o}=this.it(t.hashAttribute,this.i.stickyBucketService&&!t.disableStickyBucketing?t.fallbackAttribute:void 0);if(!o)return this.L(t,-1,!1,e);let u=-1,c=!1,a=!1;if(this.i.stickyBucketService&&!t.disableStickyBucketing){const{variation:e,versionIsBlocked:n}=this.st({expKey:t.key,expBucketVersion:t.bucketVersion,expHashAttribute:t.hashAttribute,expFallbackAttribute:t.fallbackAttribute,expMinBucketVersion:t.minBucketVersion,expMeta:t.meta});c=e>=0,u=e,a=!!n}if(!c){if(t.filters){if(this.Y(t.filters))return this.L(t,-1,!1,e)}else if(t.namespace&&!function(t,e){const n=U(\"__\"+e[0],t,1);return null!==n&&n>=e[1]&&n<e[2]}(o,t.namespace))return this.L(t,-1,!1,e);if(t.include&&!function(t){try{return t()}catch(t){return console.error(t),!1}}(t.include))return this.L(t,-1,!1,e);if(t.condition&&!this.tt(t.condition))return this.L(t,-1,!1,e);if(t.parentConditions)for(const n of t.parentConditions){const i=this.X(n.id);if(\"cyclicPrerequisite\"===i.source)return this.L(t,-1,!1,e);if(!G({value:i.value},n.condition||{}))return this.L(t,-1,!1,e)}if(t.groups&&!this.ot(t.groups))return this.L(t,-1,!1,e)}if(t.url&&!this.ut(t.url))return this.L(t,-1,!1,e);const h=U(t.seed||n,o,t.hashVersion||1);if(null===h)return this.L(t,-1,!1,e);if(c||(u=function(t,e){for(let n=0;n<e.length;n++)if(V(t,e[n]))return n;return-1}(h,t.ranges||function(t,e,n){(e=void 0===e?1:e)<0?e=0:e>1&&(e=1);const i=(r=t)<=0?[]:new Array(r).fill(1/r);var r;(n=n||i).length!==t&&(n=i);const s=n.reduce(((t,e)=>e+t),0);(s<.99||s>1.01)&&(n=i);let o=0;return n.map((t=>{const n=o;return o+=t,[n,n+e*t]}))}(i,void 0===t.coverage?1:t.coverage,t.weights))),a)return this.L(t,-1,!1,e,void 0,!0);if(u<0)return this.L(t,-1,!1,e);if(\"force\"in t)return this.L(t,void 0===t.force?-1:t.force,!1,e);if(this.i.qaMode)return this.L(t,-1,!1,e);if(\"stopped\"===t.status)return this.L(t,-1,!1,e);const l=this.L(t,u,!0,e,h,c);if(this.i.stickyBucketService&&!t.disableStickyBucketing){const{changed:e,key:n,doc:i}=this.ct(s,L(o),{[this.ht(t.key,t.bucketVersion)]:l.key});e&&(this.i.stickyBucketAssignmentDocs=this.i.stickyBucketAssignmentDocs||{},this.i.stickyBucketAssignmentDocs[n]=i,this.i.stickyBucketService.saveAssignments(i))}return this.nt(t,l),\"changeId\"in t&&t.changeId&&this.h.add(t.changeId),l}log(t,e){this.debug&&(this.i.log?this.i.log(t,e):console.log(t,e))}getDeferredTrackingCalls(){return Array.from(this.$.values())}setDeferredTrackingCalls(t){this.$=new Map(t.filter((t=>t&&t.experiment&&t.result)).map((t=>[this.lt(t.experiment,t.result),t])))}fireDeferredTrackingCalls(){this.i.trackingCallback&&(this.$.forEach((t=>{t&&t.experiment&&t.result?this.nt(t.experiment,t.result):console.error(\"Invalid deferred tracking call\",{call:t})})),this.$.clear())}setTrackingCallback(t){this.i.trackingCallback=t,this.fireDeferredTrackingCalls()}lt(t,e){return e.hashAttribute+e.hashValue+t.key+e.variationId}nt(t,e){const n=this.lt(t,e);if(this.i.trackingCallback){if(!this.u.has(n)){this.u.add(n);try{this.i.trackingCallback(t,e)}catch(t){console.error(t)}}}else this.$.has(n)||this.$.set(n,{experiment:t,result:e})}rt(t){const e=t.key,n=this.i.overrides;return n&&n[e]&&\"string\"==typeof(t=Object.assign({},t,n[e])).url&&(t.url=I(t.url)),t}it(t,e){let n=t||\"id\",i=\"\";return this.S[n]?i=this.S[n]:this.i.attributes?i=this.i.attributes[n]||\"\":this.i.user&&(i=this.i.user[n]||\"\"),!i&&e&&(this.S[e]?i=this.S[e]:this.i.attributes?i=this.i.attributes[e]||\"\":this.i.user&&(i=this.i.user[e]||\"\"),i&&(n=e)),{hashAttribute:n,hashValue:i}}L(t,e,n,i,r,s){let o=!0;(e<0||e>=t.variations.length)&&(e=0,o=!1);const{hashAttribute:u,hashValue:c}=this.it(t.hashAttribute,this.i.stickyBucketService&&!t.disableStickyBucketing?t.fallbackAttribute:void 0),a=t.meta?t.meta[e]:{},h={key:a.key||\"\"+e,featureId:i,inExperiment:o,hashUsed:n,variationId:e,value:t.variations[e],hashAttribute:u,hashValue:c,stickyBucketUsed:!!s};return a.name&&(h.name=a.name),void 0!==r&&(h.bucket=r),a.passthrough&&(h.passthrough=a.passthrough),h}q(){return this.i.url||(et?window.location.href:\"\")}ut(t){const e=this.q();if(!e)return!1;const n=e.replace(/^https?:\\/\\//,\"\").replace(/^[^/]*\\//,\"/\");return!!t.test(e)||!!t.test(n)}ot(t){const e=this.i.groups||{};for(let n=0;n<t.length;n++)if(e[t[n]])return!0;return!1}K(t){const e=P(t);if(\"visual\"===e){if(this.i.disableVisualExperiments)return!0;if(this.i.disableJsInjection&&t.variations.some((t=>t.js)))return!0}else{if(\"redirect\"!==e)return!0;if(this.i.disableUrlRedirectExperiments)return!0;try{const e=new URL(this.q());for(const n of t.variations){if(!n||!n.urlRedirect)continue;const t=new URL(n.urlRedirect);if(this.i.disableCrossOriginUrlRedirectExperiments){if(t.protocol!==e.protocol)return!0;if(t.host!==e.host)return!0}}}catch(e){return this.log(\"Error parsing current or redirect URL\",{id:t.key,error:e}),!0}}return!(!t.changeId||!(this.i.blockedChangeIds||[]).includes(t.changeId))}getRedirectUrl(){return this.O}P(){return this.i.navigate?this.i.navigate:et?t=>{window.location.replace(t)}:null}F(){if(this.i.antiFlicker&&et)try{var t;const e=document.createElement(\"style\");e.innerHTML=\".gb-anti-flicker { opacity: 0 !important; pointer-events: none; }\",document.head.appendChild(e),document.documentElement.classList.add(\"gb-anti-flicker\"),setTimeout((()=>{document.documentElement.classList.remove(\"gb-anti-flicker\")}),null!==(t=this.i.antiFlickerTimeout)&&void 0!==t?t:3500)}catch(t){console.error(t)}}G(t){if(!et)return;const e=[];if(t.css){const n=document.createElement(\"style\");n.innerHTML=t.css,document.head.appendChild(n),e.push((()=>n.remove()))}if(t.js){const n=document.createElement(\"script\");n.innerHTML=t.js,this.i.jsInjectionNonce&&(n.nonce=this.i.jsInjectionNonce),document.head.appendChild(n),e.push((()=>n.remove()))}return t.domMutations&&t.domMutations.forEach((t=>{e.push(function(t){var e=t.selector,n=t.action,r=t.value,s=t.attribute,o=t.parentSelector,u=t.insertBeforeSelector;if(\"html\"===s){if(\"append\"===n)return B(e,(function(t){return t+(null!=r?r:\"\")}));if(\"set\"===n)return B(e,(function(){return null!=r?r:\"\"}))}else if(\"class\"===s){if(\"append\"===n)return F(e,(function(t){r&&t.add(r)}));if(\"remove\"===n)return F(e,(function(t){r&&t.delete(r)}));if(\"set\"===n)return F(e,(function(t){t.clear(),r&&t.add(r)}))}else if(\"position\"===s){if(\"set\"===n&&o)return function(t,e){return R({kind:\"position\",elements:new Set,mutate:function(){return{insertBeforeSelector:u,parentSelector:o}},selector:t})}(e)}else{if(\"append\"===n)return T(e,s,(function(t){return null!==t?t+(null!=r?r:\"\"):null!=r?r:\"\"}));if(\"set\"===n)return T(e,s,(function(){return null!=r?r:\"\"}));if(\"remove\"===n)return T(e,s,(function(){return null}))}return i}(t).revert)})),()=>{e.forEach((t=>t()))}}ft(t){const e=new Set,n=t&&t.features?t.features:this.getFeatures(),i=t&&t.experiments?t.experiments:this.getExperiments();return Object.keys(n).forEach((t=>{const i=n[t];if(i.rules)for(const t of i.rules)t.variations&&(e.add(t.hashAttribute||\"id\"),t.fallbackAttribute&&e.add(t.fallbackAttribute))})),i.map((t=>{e.add(t.hashAttribute||\"id\"),t.fallbackAttribute&&e.add(t.fallbackAttribute)})),Array.from(e)}async refreshStickyBuckets(t){if(this.i.stickyBucketService){const e=this.dt(t);this.i.stickyBucketAssignmentDocs=await this.i.stickyBucketService.getAllAssignments(e)}}wt(t,e){if(!this.i.stickyBucketAssignmentDocs)return{};const{hashAttribute:n,hashValue:i}=this.it(t),r=\"\".concat(n,\"||\").concat(L(i)),{hashAttribute:s,hashValue:o}=this.it(e),u=o?\"\".concat(s,\"||\").concat(L(o)):null,c={};return u&&this.i.stickyBucketAssignmentDocs[u]&&Object.assign(c,this.i.stickyBucketAssignmentDocs[u].assignments||{}),this.i.stickyBucketAssignmentDocs[r]&&Object.assign(c,this.i.stickyBucketAssignmentDocs[r].assignments||{}),c}st(t){let{expKey:e,expBucketVersion:n,expHashAttribute:i,expFallbackAttribute:r,expMinBucketVersion:s,expMeta:o}=t;n=n||0,s=s||0,i=i||\"id\",o=o||[];const u=this.ht(e,n),c=this.wt(i,r);if(s>0)for(let t=0;t<=s;t++)if(void 0!==c[this.ht(e,t)])return{variation:-1,versionIsBlocked:!0};const a=c[u];if(void 0===a)return{variation:-1};const h=o.findIndex((t=>t.key===a));return h<0?{variation:-1}:{variation:h}}ht(t,e){return e=e||0,\"\".concat(t,\"__\").concat(e)}dt(t){const e={};return this.i.stickyBucketIdentifierAttributes=this.i.stickyBucketIdentifierAttributes?this.i.stickyBucketIdentifierAttributes:this.ft(t),this.i.stickyBucketIdentifierAttributes.forEach((t=>{const{hashValue:n}=this.it(t);e[t]=L(n)})),e}ct(t,e,n){const i=\"\".concat(t,\"||\").concat(e),r=this.i.stickyBucketAssignmentDocs&&this.i.stickyBucketAssignmentDocs[i]&&this.i.stickyBucketAssignmentDocs[i].assignments||{},s={...r,...n};return{key:i,doc:{attributeName:t,attributeValue:e,assignments:s},changed:JSON.stringify(r)!==JSON.stringify(s)}}}({...Et,remoteEval:!!Et.remoteEval,trackingCallback:(t,e)=>{const n={experiment_id:t.key,variation_id:e.key};window.gtag&&window.gtag(\"event\",\"experiment_viewed\",n),window.dataLayer&&window.dataLayer.push({event:\"experiment_viewed\",...n}),window.analytics&&window.analytics.track&&window.analytics.track(\"Experiment Viewed\",n)},...Ct,attributes:Vt(),stickyBucketService:Ft});It.setRenderer((()=>{document.dispatchEvent(new CustomEvent(\"growthbookdata\"))})),It.init({payload:Ct.payload,streaming:!(Ct.noStreaming||Et.noStreaming||!1===Ct.backgroundSync)});let Dt=location.href;setInterval((()=>{location.href!==Dt&&(Dt=location.href,It.setURL(Dt),It.updateAttributes(Vt()))}),500),document.addEventListener(\"growthbookrefresh\",(()=>{location.href!==Dt&&(Dt=location.href,It.setURL(Dt)),It.updateAttributes(Vt())})),document.addEventListener(\"growthbookpersist\",(()=>{Nt()}));const Jt=t=>{try{t&&t(It)}catch(t){console.error(\"Uncaught growthbook_queue error\",t)}};return window.growthbook_queue&&Array.isArray(window.growthbook_queue)&&window.growthbook_queue.forEach((t=>{Jt(t)})),window.growthbook_queue={push:t=>{Jt(t)}},It}();\n//# sourceMappingURL=auto.min.js.map\n";
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export {
2
+ Context,
3
+ Config,
4
+ Helpers,
5
+ ExperimentRunEnvironment,
6
+ Route,
7
+ } from "./types";
8
+ export { ConfigEnv, defaultContext, getConfig } from "./config";
9
+
10
+ export { edgeApp, getOriginUrl } from "./app";
11
+
12
+ export { getUserAttributes, getUUID, getAutoAttributes } from "./attributes";
13
+ export { applyDomMutations } from "./domMutations";
package/src/inject.ts ADDED
@@ -0,0 +1,230 @@
1
+ import {
2
+ Context as GbContext,
3
+ Attributes,
4
+ TrackingData,
5
+ AutoExperiment,
6
+ GrowthBook,
7
+ FeatureApiResponse,
8
+ getAutoExperimentChangeType,
9
+ } from "@growthbook/growthbook";
10
+ import { sdkWrapper } from "./generated/sdkWrapper";
11
+ import { Context } from "./types";
12
+
13
+ export function injectScript({
14
+ context,
15
+ body,
16
+ nonce,
17
+ growthbook,
18
+ attributes,
19
+ preRedirectChangeIds,
20
+ url,
21
+ oldUrl,
22
+ }: {
23
+ context: Context;
24
+ body: string;
25
+ nonce?: string;
26
+ growthbook: GrowthBook;
27
+ attributes: Attributes;
28
+ preRedirectChangeIds: string[];
29
+ url: string;
30
+ oldUrl: string;
31
+ }) {
32
+ if (context.config.disableInjections) return body;
33
+
34
+ const sdkPayload = growthbook.getPayload();
35
+ const experiments = growthbook.getExperiments();
36
+ const deferredTrackingCalls = growthbook.getDeferredTrackingCalls();
37
+ const completedChangeIds = growthbook.getCompletedChangeIds();
38
+
39
+ const uuidCookieName = context.config.uuidCookieName;
40
+ const uuidKey = context.config.uuidKey;
41
+ const uuid = attributes[uuidKey];
42
+ const trackingCallback = context.config.growthbook.trackingCallback;
43
+ const blockedChangeIds = getBlockedExperiments({
44
+ context,
45
+ experiments,
46
+ completedChangeIds,
47
+ preRedirectChangeIds,
48
+ });
49
+ const injectRedirectUrlScript = context.config.injectRedirectUrlScript;
50
+ const enableStreaming = context.config.enableStreaming;
51
+ const enableStickyBucketing = context.config.enableStickyBucketing;
52
+ const stickyAssignments = growthbook.getStickyBucketAssignmentDocs();
53
+
54
+ const gbContext: Omit<GbContext, "trackingCallback"> & {
55
+ uuidCookieName: string;
56
+ uuidKey: string;
57
+ uuid?: string;
58
+ attributeKeys?: Record<string, string>;
59
+ persistUuidOnLoad?: boolean;
60
+ useStickyBucketService?: "cookie" | "localStorage";
61
+ stickyBucketPrefix?: string;
62
+ trackingCallback: string; // replaced by macro
63
+ payload?: FeatureApiResponse;
64
+ noStreaming?: boolean;
65
+ } = {
66
+ uuidCookieName,
67
+ uuidKey,
68
+ uuid,
69
+ persistUuidOnLoad: true,
70
+ attributes,
71
+ trackingCallback: "__TRACKING_CALLBACK__",
72
+ payload: sdkPayload,
73
+ disableVisualExperiments: ["skip", "edge"].includes(
74
+ context.config.runVisualEditorExperiments,
75
+ ),
76
+ disableJsInjection: context.config.disableJsInjection,
77
+ disableUrlRedirectExperiments: ["skip", "edge"].includes(
78
+ context.config.runUrlRedirectExperiments,
79
+ ),
80
+ disableCrossOriginUrlRedirectExperiments: ["skip", "edge"].includes(
81
+ context.config.runCrossOriginUrlRedirectExperiments,
82
+ ),
83
+ jsInjectionNonce: nonce,
84
+ blockedChangeIds,
85
+ noStreaming: !enableStreaming,
86
+ useStickyBucketService: enableStickyBucketing ? "cookie" : undefined,
87
+ stickyBucketPrefix: context.config.stickyBucketPrefix,
88
+ stickyBucketAssignmentDocs: stickyAssignments,
89
+ };
90
+
91
+ let scriptTag = `
92
+ <script
93
+ data-api-host="${context.config.growthbook.apiHost}"
94
+ data-client-key="${context.config.growthbook.clientKey}"${
95
+ context.config.growthbook.decryptionKey
96
+ ? `\n data-decryption-key="${context.config.growthbook.decryptionKey}"`
97
+ : ""
98
+ }${nonce ? `\n nonce="${nonce}"` : ""}
99
+ >
100
+ window.growthbook_config = ${JSON.stringify(gbContext)};
101
+ ${
102
+ deferredTrackingCalls?.length
103
+ ? `
104
+ window.growthbook_queue = [
105
+ (gb) => {
106
+ gb.setDeferredTrackingCalls(${JSON.stringify(
107
+ scrubInvalidTrackingCalls(deferredTrackingCalls, preRedirectChangeIds),
108
+ )});
109
+ gb.fireDeferredTrackingCalls();
110
+ }
111
+ ];
112
+ `
113
+ : ""
114
+ }
115
+ ${sdkWrapper}
116
+ ${
117
+ url !== oldUrl && injectRedirectUrlScript
118
+ ? `window.history.replaceState(undefined, undefined, ${JSON.stringify(
119
+ url,
120
+ )})`
121
+ : ""
122
+ }
123
+ </script>
124
+ `;
125
+ scriptTag = scriptTag.replace(
126
+ `"__TRACKING_CALLBACK__"`,
127
+ trackingCallback || "undefined",
128
+ );
129
+
130
+ const index = body.indexOf(context.config.scriptInjectionPattern);
131
+ if (index >= 0) {
132
+ body = body.slice(0, index) + scriptTag + body.slice(index);
133
+ } else {
134
+ body += scriptTag;
135
+ }
136
+
137
+ return body;
138
+ }
139
+
140
+ export function getCspInfo(context: Context): {
141
+ csp?: string;
142
+ nonce?: string;
143
+ } {
144
+ // get nonce from CSP
145
+ let csp = context.config.contentSecurityPolicy;
146
+ let nonce: string | undefined = undefined;
147
+ if (csp) {
148
+ if (
149
+ (csp.indexOf("__NONCE__") || -1) >= 0 &&
150
+ context.config?.crypto?.getRandomValues
151
+ ) {
152
+ // Generate nonce
153
+ nonce = btoa(context.config.crypto.getRandomValues(new Uint32Array(2)));
154
+ csp = csp?.replace(/__NONCE__/g, nonce);
155
+ } else if (context.config?.nonce) {
156
+ // Use passed-in nonce
157
+ nonce = context.config.nonce;
158
+ }
159
+ }
160
+ // todo: support reading csp from meta tag?
161
+
162
+ return { csp, nonce };
163
+ }
164
+
165
+ function getBlockedExperiments({
166
+ context,
167
+ experiments,
168
+ completedChangeIds,
169
+ preRedirectChangeIds,
170
+ }: {
171
+ context: Context;
172
+ experiments: AutoExperiment[];
173
+ completedChangeIds: string[];
174
+ preRedirectChangeIds: string[];
175
+ }): string[] | undefined {
176
+ const runUrlRedirectExperimentsEverywhere =
177
+ context.config.runUrlRedirectExperiments === "everywhere";
178
+ const runVisualEditorExperimentsEverywhere =
179
+ context.config.runVisualEditorExperiments === "everywhere";
180
+
181
+ const blockedChangeIds: string[] = [];
182
+
183
+ if (runUrlRedirectExperimentsEverywhere) {
184
+ blockedChangeIds.concat(
185
+ ...completedChangeIds.filter((changeId) => {
186
+ // only block hybrid redirect experiments if they've already been run on edge
187
+ const exp = experiments.find(
188
+ (exp) =>
189
+ getAutoExperimentChangeType(exp) === "redirect" &&
190
+ exp.changeId === changeId,
191
+ );
192
+ return !!(exp?.changeId && completedChangeIds.includes(exp.changeId));
193
+ }),
194
+ );
195
+ }
196
+
197
+ if (runVisualEditorExperimentsEverywhere) {
198
+ blockedChangeIds.concat(
199
+ ...preRedirectChangeIds.filter((changeId) => {
200
+ // only block hybrid visual experiments if they've already been run on edge as part of the pre-redirect loop
201
+ const exp = experiments.find(
202
+ (exp) =>
203
+ getAutoExperimentChangeType(exp) === "visual" &&
204
+ exp.changeId === changeId,
205
+ );
206
+ return !!(exp?.changeId && completedChangeIds.includes(exp.changeId));
207
+ }),
208
+ );
209
+ }
210
+
211
+ return blockedChangeIds.length ? blockedChangeIds : undefined;
212
+ }
213
+
214
+ function scrubInvalidTrackingCalls(
215
+ deferredTrackingCalls: TrackingData[],
216
+ preRedirectChangeIds: string[],
217
+ ): TrackingData[] {
218
+ return deferredTrackingCalls.filter((data) => {
219
+ const exp = data.experiment as AutoExperiment;
220
+ // remove tracking for any visual experiments that ran during the redirect loop
221
+ if (
222
+ getAutoExperimentChangeType(exp) === "visual" &&
223
+ exp.changeId &&
224
+ preRedirectChangeIds.includes(exp.changeId)
225
+ ) {
226
+ return false;
227
+ }
228
+ return true;
229
+ });
230
+ }
@@ -0,0 +1,53 @@
1
+ import { GrowthBook } from "@growthbook/growthbook";
2
+ import { Context } from "./types";
3
+ import { getUserAttributes } from "./attributes";
4
+
5
+ export default async function redirect({
6
+ context,
7
+ req,
8
+ setCookie,
9
+ growthbook,
10
+ previousUrl,
11
+ resetDomChanges,
12
+ setPreRedirectChangeIds,
13
+ }: {
14
+ context: Context;
15
+ req: unknown;
16
+ setCookie: (key: string, value: string) => void;
17
+ growthbook: GrowthBook;
18
+ previousUrl: string;
19
+ resetDomChanges: () => void;
20
+ setPreRedirectChangeIds: (changeIds: string[]) => void;
21
+ }): Promise<string> {
22
+ const disableUrlRedirectExperiments = ["skip", "browser"].includes(
23
+ context.config.runUrlRedirectExperiments,
24
+ );
25
+ if (disableUrlRedirectExperiments) return previousUrl;
26
+
27
+ const maxRedirects = context.config.maxRedirects || 5;
28
+ let redirectCount = 0;
29
+
30
+ let newUrl = growthbook.getRedirectUrl();
31
+ // no redirect
32
+ if (!newUrl) return previousUrl;
33
+
34
+ while (previousUrl != newUrl) {
35
+ newUrl = previousUrl;
36
+ if (redirectCount >= maxRedirects) return previousUrl;
37
+
38
+ // clear visual experiment effects since we're no longer on the same page
39
+ resetDomChanges();
40
+ // keep track of experiments that triggered prior to final redirect
41
+ setPreRedirectChangeIds(growthbook.getCompletedChangeIds());
42
+
43
+ // change the URL to trigger the experiment
44
+ await growthbook.setAttributes(
45
+ getUserAttributes(context, req, newUrl, setCookie),
46
+ );
47
+ await growthbook.setURL(newUrl);
48
+ previousUrl = newUrl;
49
+ newUrl = growthbook.getRedirectUrl();
50
+ redirectCount++;
51
+ }
52
+ return newUrl;
53
+ }
package/src/routing.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { isURLTargeted } from "@growthbook/growthbook";
2
+ import { Context, Route } from "./types";
3
+
4
+ export function getRoute(context: Context, url: string): Route {
5
+ const routes = context.config.routes || [];
6
+ for (const route of routes) {
7
+ route.type = route.type ?? "simple";
8
+ route.behavior = route.behavior ?? "intercept";
9
+ route.includeFileExtensions = !!route.includeFileExtensions;
10
+ if (route.behavior === "error") {
11
+ route.statusCode = route.statusCode ?? 404;
12
+ }
13
+
14
+ const target = {
15
+ include: true,
16
+ type: route.type ?? "simple",
17
+ pattern: route.pattern,
18
+ };
19
+ const targeted = isURLTargeted(url, [target]);
20
+
21
+ if (targeted) {
22
+ if (route.includeFileExtensions) {
23
+ return route;
24
+ }
25
+ try {
26
+ const urlObj = new URL(url);
27
+ const path = urlObj.pathname;
28
+ if (path.includes(".")) {
29
+ // extensions will proxy if not explicitly included
30
+ return { ...route, behavior: "proxy" };
31
+ }
32
+ } catch (e) {
33
+ // ignore
34
+ }
35
+ return route;
36
+ }
37
+ }
38
+ // Default route is intercept & process
39
+ return {
40
+ pattern: "*",
41
+ type: "simple",
42
+ behavior: "intercept",
43
+ };
44
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ StickyAssignmentsDocument,
3
+ StickyBucketService,
4
+ } from "@growthbook/growthbook";
5
+ import { Context } from "./types";
6
+
7
+ export class EdgeStickyBucketService<Req, Res> extends StickyBucketService {
8
+ private context: Context<Req, Res>;
9
+ private prefix: string;
10
+ private req: Req;
11
+
12
+ constructor({
13
+ context,
14
+ prefix = "gbStickyBuckets__",
15
+ req,
16
+ }: {
17
+ context: Context<Req, Res>;
18
+ prefix?: string;
19
+ req: Req;
20
+ }) {
21
+ super();
22
+ this.context = context;
23
+ this.prefix = prefix;
24
+ this.req = req;
25
+ }
26
+
27
+ async getAssignments(attributeName: string, attributeValue: string) {
28
+ const key = `${attributeName}||${attributeValue}`;
29
+ let doc: StickyAssignmentsDocument | null = null;
30
+ if (!this.req) return doc;
31
+ try {
32
+ // const raw = this.req.cookies[this.prefix + key] || "{}";
33
+ const raw =
34
+ this.context.helpers?.getCookie?.(this.req, this.prefix + key) || "{}";
35
+ const data = JSON.parse(raw);
36
+ if (data.attributeName && data.attributeValue && data.assignments) {
37
+ doc = data;
38
+ }
39
+ } catch (e) {
40
+ // Ignore cookie errors
41
+ }
42
+ return doc;
43
+ }
44
+
45
+ async saveAssignments(_: StickyAssignmentsDocument) {
46
+ // Do nothing. User assignments will be hydrated from the SDK directly.
47
+ }
48
+ }