var _growthbook = (function () {
  'use strict';

  /*! js-cookie v3.0.5 | MIT */
  /* eslint-disable no-var */
  function assign (target) {
    for (var i = 1; i < arguments.length; i++) {
      var source = arguments[i];
      for (var key in source) {
        target[key] = source[key];
      }
    }
    return target
  }
  /* eslint-enable no-var */

  /* eslint-disable no-var */
  var defaultConverter = {
    read: function (value) {
      if (value[0] === '"') {
        value = value.slice(1, -1);
      }
      return value.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent)
    },
    write: function (value) {
      return encodeURIComponent(value).replace(
        /%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,
        decodeURIComponent
      )
    }
  };
  /* eslint-enable no-var */

  /* eslint-disable no-var */

  function init (converter, defaultAttributes) {
    function set (name, value, attributes) {
      if (typeof document === 'undefined') {
        return
      }

      attributes = assign({}, defaultAttributes, attributes);

      if (typeof attributes.expires === 'number') {
        attributes.expires = new Date(Date.now() + attributes.expires * 864e5);
      }
      if (attributes.expires) {
        attributes.expires = attributes.expires.toUTCString();
      }

      name = encodeURIComponent(name)
        .replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent)
        .replace(/[()]/g, escape);

      var stringifiedAttributes = '';
      for (var attributeName in attributes) {
        if (!attributes[attributeName]) {
          continue
        }

        stringifiedAttributes += '; ' + attributeName;

        if (attributes[attributeName] === true) {
          continue
        }

        // Considers RFC 6265 section 5.2:
        // ...
        // 3.  If the remaining unparsed-attributes contains a %x3B (";")
        //     character:
        // Consume the characters of the unparsed-attributes up to,
        // not including, the first %x3B (";") character.
        // ...
        stringifiedAttributes += '=' + attributes[attributeName].split(';')[0];
      }

      return (document.cookie =
        name + '=' + converter.write(value, name) + stringifiedAttributes)
    }

    function get (name) {
      if (typeof document === 'undefined' || (arguments.length && !name)) {
        return
      }

      // To prevent the for loop in the first place assign an empty array
      // in case there are no cookies at all.
      var cookies = document.cookie ? document.cookie.split('; ') : [];
      var jar = {};
      for (var i = 0; i < cookies.length; i++) {
        var parts = cookies[i].split('=');
        var value = parts.slice(1).join('=');

        try {
          var found = decodeURIComponent(parts[0]);
          jar[found] = converter.read(value, found);

          if (name === found) {
            break
          }
        } catch (e) {}
      }

      return name ? jar[name] : jar
    }

    return Object.create(
      {
        set,
        get,
        remove: function (name, attributes) {
          set(
            name,
            '',
            assign({}, attributes, {
              expires: -1
            })
          );
        },
        withAttributes: function (attributes) {
          return init(this.converter, assign({}, this.attributes, attributes))
        },
        withConverter: function (converter) {
          return init(assign({}, this.converter, converter), this.attributes)
        }
      },
      {
        attributes: { value: Object.freeze(defaultAttributes) },
        converter: { value: Object.freeze(converter) }
      }
    )
  }

  var api = init(defaultConverter, { path: '/' });

  var validAttributeName = /^[a-zA-Z:_][a-zA-Z0-9:_.-]*$/;
  var nullController = {
    revert: function revert() {}
  };
  var elements = /*#__PURE__*/new Map();
  var mutations = /*#__PURE__*/new Set();
  function getObserverInit(attr) {
    return attr === 'html' ? {
      childList: true,
      subtree: true,
      attributes: true,
      characterData: true
    } : {
      childList: false,
      subtree: false,
      attributes: true,
      attributeFilter: [attr]
    };
  }
  function getElementRecord(element) {
    var record = elements.get(element);
    if (!record) {
      record = {
        element: element,
        attributes: {}
      };
      elements.set(element, record);
    }
    return record;
  }
  function createElementPropertyRecord(el, attr, getCurrentValue, setValue, mutationRunner) {
    var currentValue = getCurrentValue(el);
    var record = {
      isDirty: false,
      originalValue: currentValue,
      virtualValue: currentValue,
      mutations: [],
      el: el,
      _positionTimeout: null,
      observer: new MutationObserver(function () {
        // enact a 1 second timeout that blocks subsequent firing of the
        // observer until the timeout is complete. This will prevent multiple
        // mutations from firing in quick succession, which can cause the
        // mutation to be reverted before the DOM has a chance to update.
        if (attr === 'position' && record._positionTimeout) return;else if (attr === 'position') record._positionTimeout = setTimeout(function () {
          record._positionTimeout = null;
        }, 1000);
        var currentValue = getCurrentValue(el);
        if (attr === 'position' && currentValue.parentNode === record.virtualValue.parentNode && currentValue.insertBeforeNode === record.virtualValue.insertBeforeNode) return;
        if (currentValue === record.virtualValue) return;
        record.originalValue = currentValue;
        mutationRunner(record);
      }),
      mutationRunner: mutationRunner,
      setValue: setValue,
      getCurrentValue: getCurrentValue
    };
    if (attr === 'position' && el.parentNode) {
      record.observer.observe(el.parentNode, {
        childList: true,
        subtree: true,
        attributes: false,
        characterData: false
      });
    } else {
      record.observer.observe(el, getObserverInit(attr));
    }
    return record;
  }
  function queueIfNeeded(val, record) {
    var currentVal = record.getCurrentValue(record.el);
    record.virtualValue = val;
    if (val && typeof val !== 'string') {
      if (!currentVal || val.parentNode !== currentVal.parentNode || val.insertBeforeNode !== currentVal.insertBeforeNode) {
        record.isDirty = true;
        runDOMUpdates();
      }
    } else if (val !== currentVal) {
      record.isDirty = true;
      runDOMUpdates();
    }
  }
  function htmlMutationRunner(record) {
    var val = record.originalValue;
    record.mutations.forEach(function (m) {
      return val = m.mutate(val);
    });
    queueIfNeeded(getTransformedHTML(val), record);
  }
  function classMutationRunner(record) {
    var val = new Set(record.originalValue.split(/\s+/).filter(Boolean));
    record.mutations.forEach(function (m) {
      return m.mutate(val);
    });
    queueIfNeeded(Array.from(val).filter(Boolean).join(' '), record);
  }
  function attrMutationRunner(record) {
    var val = record.originalValue;
    record.mutations.forEach(function (m) {
      return val = m.mutate(val);
    });
    queueIfNeeded(val, record);
  }
  function _loadDOMNodes(_ref) {
    var parentSelector = _ref.parentSelector,
      insertBeforeSelector = _ref.insertBeforeSelector;
    var parentNode = document.querySelector(parentSelector);
    if (!parentNode) return null;
    var insertBeforeNode = insertBeforeSelector ? document.querySelector(insertBeforeSelector) : null;
    if (insertBeforeSelector && !insertBeforeNode) return null;
    return {
      parentNode: parentNode,
      insertBeforeNode: insertBeforeNode
    };
  }
  function positionMutationRunner(record) {
    var val = record.originalValue;
    record.mutations.forEach(function (m) {
      var selectors = m.mutate();
      var newNodes = _loadDOMNodes(selectors);
      val = newNodes || val;
    });
    queueIfNeeded(val, record);
  }
  var getHTMLValue = function getHTMLValue(el) {
    return el.innerHTML;
  };
  var setHTMLValue = function setHTMLValue(el, value) {
    return el.innerHTML = value;
  };
  function getElementHTMLRecord(element) {
    var elementRecord = getElementRecord(element);
    if (!elementRecord.html) {
      elementRecord.html = createElementPropertyRecord(element, 'html', getHTMLValue, setHTMLValue, htmlMutationRunner);
    }
    return elementRecord.html;
  }
  var getElementPosition = function getElementPosition(el) {
    return {
      parentNode: el.parentElement,
      insertBeforeNode: el.nextElementSibling
    };
  };
  var setElementPosition = function setElementPosition(el, value) {
    if (value.insertBeforeNode && !value.parentNode.contains(value.insertBeforeNode)) {
      // skip position mutation - insertBeforeNode not a child of parent. happens
      // when mutation observer for indvidual element fires out of order
      return;
    }
    value.parentNode.insertBefore(el, value.insertBeforeNode);
  };
  function getElementPositionRecord(element) {
    var elementRecord = getElementRecord(element);
    if (!elementRecord.position) {
      elementRecord.position = createElementPropertyRecord(element, 'position', getElementPosition, setElementPosition, positionMutationRunner);
    }
    return elementRecord.position;
  }
  var setClassValue = function setClassValue(el, val) {
    return val ? el.className = val : el.removeAttribute('class');
  };
  var getClassValue = function getClassValue(el) {
    return el.className;
  };
  function getElementClassRecord(el) {
    var elementRecord = getElementRecord(el);
    if (!elementRecord.classes) {
      elementRecord.classes = createElementPropertyRecord(el, 'class', getClassValue, setClassValue, classMutationRunner);
    }
    return elementRecord.classes;
  }
  var getAttrValue = function getAttrValue(attrName) {
    return function (el) {
      var _el$getAttribute;
      return (_el$getAttribute = el.getAttribute(attrName)) != null ? _el$getAttribute : null;
    };
  };
  var setAttrValue = function setAttrValue(attrName) {
    return function (el, val) {
      return val !== null ? el.setAttribute(attrName, val) : el.removeAttribute(attrName);
    };
  };
  function getElementAttributeRecord(el, attr) {
    var elementRecord = getElementRecord(el);
    if (!elementRecord.attributes[attr]) {
      elementRecord.attributes[attr] = createElementPropertyRecord(el, attr, getAttrValue(attr), setAttrValue(attr), attrMutationRunner);
    }
    return elementRecord.attributes[attr];
  }
  function deleteElementPropertyRecord(el, attr) {
    var element = elements.get(el);
    if (!element) return;
    if (attr === 'html') {
      var _element$html, _element$html$observe;
      (_element$html = element.html) == null ? void 0 : (_element$html$observe = _element$html.observer) == null ? void 0 : _element$html$observe.disconnect();
      delete element.html;
    } else if (attr === 'class') {
      var _element$classes, _element$classes$obse;
      (_element$classes = element.classes) == null ? void 0 : (_element$classes$obse = _element$classes.observer) == null ? void 0 : _element$classes$obse.disconnect();
      delete element.classes;
    } else if (attr === 'position') {
      var _element$position, _element$position$obs;
      (_element$position = element.position) == null ? void 0 : (_element$position$obs = _element$position.observer) == null ? void 0 : _element$position$obs.disconnect();
      delete element.position;
    } else {
      var _element$attributes, _element$attributes$a, _element$attributes$a2;
      (_element$attributes = element.attributes) == null ? void 0 : (_element$attributes$a = _element$attributes[attr]) == null ? void 0 : (_element$attributes$a2 = _element$attributes$a.observer) == null ? void 0 : _element$attributes$a2.disconnect();
      delete element.attributes[attr];
    }
  }
  var transformContainer;
  function getTransformedHTML(html) {
    if (!transformContainer) {
      transformContainer = document.createElement('div');
    }
    transformContainer.innerHTML = html;
    return transformContainer.innerHTML;
  }
  function setPropertyValue(el, attr, m) {
    if (!m.isDirty) return;
    m.isDirty = false;
    var val = m.virtualValue;
    if (!m.mutations.length) {
      deleteElementPropertyRecord(el, attr);
    }
    m.setValue(el, val);
  }
  function setValue(m, el) {
    m.html && setPropertyValue(el, 'html', m.html);
    m.classes && setPropertyValue(el, 'class', m.classes);
    m.position && setPropertyValue(el, 'position', m.position);
    Object.keys(m.attributes).forEach(function (attr) {
      setPropertyValue(el, attr, m.attributes[attr]);
    });
  }
  function runDOMUpdates() {
    elements.forEach(setValue);
  } // find or create ElementPropertyRecord, add mutation to it, then run

  function startMutating(mutation, element) {
    var record = null;
    if (mutation.kind === 'html') {
      record = getElementHTMLRecord(element);
    } else if (mutation.kind === 'class') {
      record = getElementClassRecord(element);
    } else if (mutation.kind === 'attribute') {
      record = getElementAttributeRecord(element, mutation.attribute);
    } else if (mutation.kind === 'position') {
      record = getElementPositionRecord(element);
    }
    if (!record) return;
    record.mutations.push(mutation);
    record.mutationRunner(record);
  } // get (existing) ElementPropertyRecord, remove mutation from it, then run

  function stopMutating(mutation, el) {
    var record = null;
    if (mutation.kind === 'html') {
      record = getElementHTMLRecord(el);
    } else if (mutation.kind === 'class') {
      record = getElementClassRecord(el);
    } else if (mutation.kind === 'attribute') {
      record = getElementAttributeRecord(el, mutation.attribute);
    } else if (mutation.kind === 'position') {
      record = getElementPositionRecord(el);
    }
    if (!record) return;
    var index = record.mutations.indexOf(mutation);
    if (index !== -1) record.mutations.splice(index, 1);
    record.mutationRunner(record);
  } // maintain list of elements associated with mutation

  function refreshElementsSet(mutation) {
    // if a position mutation has already found an element to move, don't move
    // any more elements
    if (mutation.kind === 'position' && mutation.elements.size === 1) return;
    var existingElements = new Set(mutation.elements);
    var matchingElements = document.querySelectorAll(mutation.selector);
    matchingElements.forEach(function (el) {
      if (!existingElements.has(el)) {
        mutation.elements.add(el);
        startMutating(mutation, el);
      }
    });
  }
  function revertMutation(mutation) {
    mutation.elements.forEach(function (el) {
      return stopMutating(mutation, el);
    });
    mutation.elements.clear();
    mutations["delete"](mutation);
  }
  function refreshAllElementSets() {
    mutations.forEach(refreshElementsSet);
  } // Observer for elements that don't exist in the DOM yet

  var observer;
  function connectGlobalObserver() {
    if (typeof document === 'undefined') return;
    if (!observer) {
      observer = new MutationObserver(function () {
        refreshAllElementSets();
      });
    }
    refreshAllElementSets();
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: false,
      characterData: false
    });
  } // run on init

  connectGlobalObserver();
  function newMutation(m) {
    // Not in a browser
    if (typeof document === 'undefined') return nullController; // add to global index of mutations

    mutations.add(m); // run refresh on init to establish list of elements associated w/ mutation

    refreshElementsSet(m);
    return {
      revert: function revert() {
        revertMutation(m);
      }
    };
  }
  function html(selector, mutate) {
    return newMutation({
      kind: 'html',
      elements: new Set(),
      mutate: mutate,
      selector: selector
    });
  }
  function position(selector, mutate) {
    return newMutation({
      kind: 'position',
      elements: new Set(),
      mutate: mutate,
      selector: selector
    });
  }
  function classes(selector, mutate) {
    return newMutation({
      kind: 'class',
      elements: new Set(),
      mutate: mutate,
      selector: selector
    });
  }
  function attribute(selector, attribute, mutate) {
    if (!validAttributeName.test(attribute)) return nullController;
    if (attribute === 'class' || attribute === 'className') {
      return classes(selector, function (classnames) {
        var mutatedClassnames = mutate(Array.from(classnames).join(' '));
        classnames.clear();
        if (!mutatedClassnames) return;
        mutatedClassnames.split(/\s+/g).filter(Boolean).forEach(function (c) {
          return classnames.add(c);
        });
      });
    }
    return newMutation({
      kind: 'attribute',
      attribute: attribute,
      elements: new Set(),
      mutate: mutate,
      selector: selector
    });
  }
  function declarative(_ref2) {
    var selector = _ref2.selector,
      action = _ref2.action,
      value = _ref2.value,
      attr = _ref2.attribute,
      parentSelector = _ref2.parentSelector,
      insertBeforeSelector = _ref2.insertBeforeSelector;
    if (attr === 'html') {
      if (action === 'append') {
        return html(selector, function (val) {
          return val + (value != null ? value : '');
        });
      } else if (action === 'set') {
        return html(selector, function () {
          return value != null ? value : '';
        });
      }
    } else if (attr === 'class') {
      if (action === 'append') {
        return classes(selector, function (val) {
          if (value) val.add(value);
        });
      } else if (action === 'remove') {
        return classes(selector, function (val) {
          if (value) val["delete"](value);
        });
      } else if (action === 'set') {
        return classes(selector, function (val) {
          val.clear();
          if (value) val.add(value);
        });
      }
    } else if (attr === 'position') {
      if (action === 'set' && parentSelector) {
        return position(selector, function () {
          return {
            insertBeforeSelector: insertBeforeSelector,
            parentSelector: parentSelector
          };
        });
      }
    } else {
      if (action === 'append') {
        return attribute(selector, attr, function (val) {
          return val !== null ? val + (value != null ? value : '') : value != null ? value : '';
        });
      } else if (action === 'set') {
        return attribute(selector, attr, function () {
          return value != null ? value : '';
        });
      } else if (action === 'remove') {
        return attribute(selector, attr, function () {
          return null;
        });
      }
    }
    return nullController;
  }
  var index = {
    html: html,
    classes: classes,
    attribute: attribute,
    position: position,
    declarative: declarative
  };

  const polyfills$1 = {
    fetch: globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined,
    SubtleCrypto: globalThis.crypto ? globalThis.crypto.subtle : undefined,
    EventSource: globalThis.EventSource
  };
  function getPolyfills() {
    return polyfills$1;
  }
  function hashFnv32a(str) {
    let hval = 0x811c9dc5;
    const l = str.length;
    for (let i = 0; i < l; i++) {
      hval ^= str.charCodeAt(i);
      hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
    }
    return hval >>> 0;
  }
  function hash(seed, value, version) {
    // New unbiased hashing algorithm
    if (version === 2) {
      return hashFnv32a(hashFnv32a(seed + value) + "") % 10000 / 10000;
    }
    // Original biased hashing algorithm (keep for backwards compatibility)
    if (version === 1) {
      return hashFnv32a(value + seed) % 1000 / 1000;
    }

    // Unknown hash version
    return null;
  }
  function getEqualWeights(n) {
    if (n <= 0) return [];
    return new Array(n).fill(1 / n);
  }
  function inRange(n, range) {
    return n >= range[0] && n < range[1];
  }
  function inNamespace(hashValue, namespace) {
    const n = hash("__" + namespace[0], hashValue, 1);
    if (n === null) return false;
    return n >= namespace[1] && n < namespace[2];
  }
  function chooseVariation(n, ranges) {
    for (let i = 0; i < ranges.length; i++) {
      if (inRange(n, ranges[i])) {
        return i;
      }
    }
    return -1;
  }
  function getUrlRegExp(regexString) {
    try {
      const escaped = regexString.replace(/([^\\])\//g, "$1\\/");
      return new RegExp(escaped);
    } catch (e) {
      console.error(e);
      return undefined;
    }
  }
  function isURLTargeted(url, targets) {
    if (!targets.length) return false;
    let hasIncludeRules = false;
    let isIncluded = false;
    for (let i = 0; i < targets.length; i++) {
      const match = _evalURLTarget(url, targets[i].type, targets[i].pattern);
      if (targets[i].include === false) {
        if (match) return false;
      } else {
        hasIncludeRules = true;
        if (match) isIncluded = true;
      }
    }
    return isIncluded || !hasIncludeRules;
  }
  function _evalSimpleUrlPart(actual, pattern, isPath) {
    try {
      // Escape special regex characters and change wildcard `_____` to `.*`
      let escaped = pattern.replace(/[*.+?^${}()|[\]\\]/g, "\\$&").replace(/_____/g, ".*");
      if (isPath) {
        // When matching pathname, make leading/trailing slashes optional
        escaped = "\\/?" + escaped.replace(/(^\/|\/$)/g, "") + "\\/?";
      }
      const regex = new RegExp("^" + escaped + "$", "i");
      return regex.test(actual);
    } catch (e) {
      return false;
    }
  }
  function _evalSimpleUrlTarget(actual, pattern) {
    try {
      // If a protocol is missing, but a host is specified, add `https://` to the front
      // Use "_____" as the wildcard since `*` is not a valid hostname in some browsers
      const expected = new URL(pattern.replace(/^([^:/?]*)\./i, "https://$1.").replace(/\*/g, "_____"), "https://_____");

      // Compare each part of the URL separately
      const comps = [[actual.host, expected.host, false], [actual.pathname, expected.pathname, true]];
      // We only want to compare hashes if it's explicitly being targeted
      if (expected.hash) {
        comps.push([actual.hash, expected.hash, false]);
      }
      expected.searchParams.forEach((v, k) => {
        comps.push([actual.searchParams.get(k) || "", v, false]);
      });

      // If any comparisons fail, the whole thing fails
      return !comps.some(data => !_evalSimpleUrlPart(data[0], data[1], data[2]));
    } catch (e) {
      return false;
    }
  }
  function _evalURLTarget(url, type, pattern) {
    try {
      const parsed = new URL(url, "https://_");
      if (type === "regex") {
        const regex = getUrlRegExp(pattern);
        if (!regex) return false;
        return regex.test(parsed.href) || regex.test(parsed.href.substring(parsed.origin.length));
      } else if (type === "simple") {
        return _evalSimpleUrlTarget(parsed, pattern);
      }
      return false;
    } catch (e) {
      return false;
    }
  }
  function getBucketRanges(numVariations, coverage, weights) {
    coverage = coverage === undefined ? 1 : coverage;

    // Make sure coverage is within bounds
    if (coverage < 0) {
      coverage = 0;
    } else if (coverage > 1) {
      coverage = 1;
    }

    // Default to equal weights if missing or invalid
    const equal = getEqualWeights(numVariations);
    weights = weights || equal;
    if (weights.length !== numVariations) {
      weights = equal;
    }

    // If weights don't add up to 1 (or close to it), default to equal weights
    const totalWeight = weights.reduce((w, sum) => sum + w, 0);
    if (totalWeight < 0.99 || totalWeight > 1.01) {
      weights = equal;
    }

    // Covert weights to ranges
    let cumulative = 0;
    return weights.map(w => {
      const start = cumulative;
      cumulative += w;
      return [start, start + coverage * w];
    });
  }
  function getQueryStringOverride(id, url, numVariations) {
    if (!url) {
      return null;
    }
    const search = url.split("?")[1];
    if (!search) {
      return null;
    }
    const match = search.replace(/#.*/, "") // Get rid of anchor
    .split("&") // Split into key/value pairs
    .map(kv => kv.split("=", 2)).filter(([k]) => k === id) // Look for key that matches the experiment id
    .map(([, v]) => parseInt(v)); // Parse the value into an integer

    if (match.length > 0 && match[0] >= 0 && match[0] < numVariations) return match[0];
    return null;
  }
  function isIncluded(include) {
    try {
      return include();
    } catch (e) {
      console.error(e);
      return false;
    }
  }
  const base64ToBuf = b => Uint8Array.from(atob(b), c => c.charCodeAt(0));
  async function decrypt(encryptedString, decryptionKey, subtle) {
    decryptionKey = decryptionKey || "";
    subtle = subtle || globalThis.crypto && globalThis.crypto.subtle || polyfills$1.SubtleCrypto;
    if (!subtle) {
      throw new Error("No SubtleCrypto implementation found");
    }
    try {
      const key = await subtle.importKey("raw", base64ToBuf(decryptionKey), {
        name: "AES-CBC",
        length: 128
      }, true, ["encrypt", "decrypt"]);
      const [iv, cipherText] = encryptedString.split(".");
      const plainTextBuffer = await subtle.decrypt({
        name: "AES-CBC",
        iv: base64ToBuf(iv)
      }, key, base64ToBuf(cipherText));
      return new TextDecoder().decode(plainTextBuffer);
    } catch (e) {
      throw new Error("Failed to decrypt");
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  function toString(input) {
    if (typeof input === "string") return input;
    return JSON.stringify(input);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  function paddedVersionString(input) {
    if (typeof input === "number") {
      input = input + "";
    }
    if (!input || typeof input !== "string") {
      input = "0";
    }
    // Remove build info and leading `v` if any
    // Split version into parts (both core version numbers and pre-release tags)
    // "v1.2.3-rc.1+build123" -> ["1","2","3","rc","1"]
    const parts = input.replace(/(^v|\+.*$)/g, "").split(/[-.]/);

    // If it's SemVer without a pre-release, add `~` to the end
    // ["1","0","0"] -> ["1","0","0","~"]
    // "~" is the largest ASCII character, so this will make "1.0.0" greater than "1.0.0-beta" for example
    if (parts.length === 3) {
      parts.push("~");
    }

    // Left pad each numeric part with spaces so string comparisons will work ("9">"10", but " 9"<"10")
    // Then, join back together into a single string
    return parts.map(v => v.match(/^[0-9]+$/) ? v.padStart(5, " ") : v).join("-");
  }
  function loadSDKVersion() {
    let version;
    try {
      // @ts-expect-error right-hand value to be replaced by build with string literal
      version = "1.6.5";
    } catch (e) {
      version = "";
    }
    return version;
  }
  function mergeQueryStrings(oldUrl, newUrl) {
    let currUrl;
    let redirectUrl;
    try {
      currUrl = new URL(oldUrl);
      redirectUrl = new URL(newUrl);
    } catch (e) {
      console.error(`Unable to merge query strings: ${e}`);
      return newUrl;
    }
    currUrl.searchParams.forEach((value, key) => {
      // skip  if search param already exists in redirectUrl
      if (redirectUrl.searchParams.has(key)) {
        return;
      }
      redirectUrl.searchParams.set(key, value);
    });
    return redirectUrl.toString();
  }
  function isObj(x) {
    return typeof x === "object" && x !== null;
  }
  function getAutoExperimentChangeType(exp) {
    if (exp.urlPatterns && exp.variations.some(variation => isObj(variation) && "urlRedirect" in variation)) {
      return "redirect";
    } else if (exp.variations.some(variation => isObj(variation) && (variation.domMutations || "js" in variation || "css" in variation))) {
      return "visual";
    }
    return "unknown";
  }

  // Guarantee the promise always resolves within {timeout} ms
  // Resolved value will be `null` when there's an error or it takes too long
  // Note: The promise will continue running in the background, even if the timeout is hit
  async function promiseTimeout(promise, timeout) {
    return new Promise(resolve => {
      let resolved = false;
      let timer;
      const finish = data => {
        if (resolved) return;
        resolved = true;
        timer && clearTimeout(timer);
        resolve(data || null);
      };
      if (timeout) {
        timer = setTimeout(() => finish(), timeout);
      }
      promise.then(data => finish(data)).catch(() => finish());
    });
  }

  // Config settings
  const cacheSettings = {
    // Consider a fetch stale after 1 minute
    staleTTL: 1000 * 60,
    // Max time to keep a fetch in cache (4 hours default)
    maxAge: 1000 * 60 * 60 * 4,
    cacheKey: "gbFeaturesCache",
    backgroundSync: true,
    maxEntries: 10,
    disableIdleStreams: false,
    idleStreamInterval: 20000,
    disableCache: false
  };
  const polyfills = getPolyfills();
  const helpers = {
    fetchFeaturesCall: ({
      host,
      clientKey,
      headers
    }) => {
      return polyfills.fetch(`${host}/api/features/${clientKey}`, {
        headers
      });
    },
    fetchRemoteEvalCall: ({
      host,
      clientKey,
      payload,
      headers
    }) => {
      const options = {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          ...headers
        },
        body: JSON.stringify(payload)
      };
      return polyfills.fetch(`${host}/api/eval/${clientKey}`, options);
    },
    eventSourceCall: ({
      host,
      clientKey,
      headers
    }) => {
      if (headers) {
        return new polyfills.EventSource(`${host}/sub/${clientKey}`, {
          headers
        });
      }
      return new polyfills.EventSource(`${host}/sub/${clientKey}`);
    },
    startIdleListener: () => {
      let idleTimeout;
      const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
      if (!isBrowser) return;
      const onVisibilityChange = () => {
        if (document.visibilityState === "visible") {
          window.clearTimeout(idleTimeout);
          onVisible();
        } else if (document.visibilityState === "hidden") {
          idleTimeout = window.setTimeout(onHidden, cacheSettings.idleStreamInterval);
        }
      };
      document.addEventListener("visibilitychange", onVisibilityChange);
      return () => document.removeEventListener("visibilitychange", onVisibilityChange);
    },
    stopIdleListener: () => {
      // No-op, replaced by startIdleListener
    }
  };
  try {
    if (globalThis.localStorage) {
      polyfills.localStorage = globalThis.localStorage;
    }
  } catch (e) {
    // Ignore localStorage errors
  }

  // Global state
  const subscribedInstances = new Map();
  let cacheInitialized = false;
  const cache = new Map();
  const activeFetches = new Map();
  const streams = new Map();
  const supportsSSE = new Set();
  function configureCache(overrides) {
    Object.assign(cacheSettings, overrides);
    if (!cacheSettings.backgroundSync) {
      clearAutoRefresh();
    }
  }

  // Get or fetch features and refresh the SDK instance
  async function refreshFeatures({
    instance,
    timeout,
    skipCache,
    allowStale,
    backgroundSync
  }) {
    if (!backgroundSync) {
      cacheSettings.backgroundSync = false;
    }
    return fetchFeaturesWithCache({
      instance,
      allowStale,
      timeout,
      skipCache
    });
  }

  // Subscribe a GrowthBook instance to feature changes
  function subscribe(instance) {
    const key = getKey(instance);
    const subs = subscribedInstances.get(key) || new Set();
    subs.add(instance);
    subscribedInstances.set(key, subs);
  }
  function unsubscribe(instance) {
    subscribedInstances.forEach(s => s.delete(instance));
  }
  function onHidden() {
    streams.forEach(channel => {
      if (!channel) return;
      channel.state = "idle";
      disableChannel(channel);
    });
  }
  function onVisible() {
    streams.forEach(channel => {
      if (!channel) return;
      if (channel.state !== "idle") return;
      enableChannel(channel);
    });
  }

  // Private functions

  async function updatePersistentCache() {
    try {
      if (!polyfills.localStorage) return;
      await polyfills.localStorage.setItem(cacheSettings.cacheKey, JSON.stringify(Array.from(cache.entries())));
    } catch (e) {
      // Ignore localStorage errors
    }
  }

  // SWR wrapper for fetching features. May indirectly or directly start SSE streaming.
  async function fetchFeaturesWithCache({
    instance,
    allowStale,
    timeout,
    skipCache
  }) {
    const key = getKey(instance);
    const cacheKey = getCacheKey(instance);
    const now = new Date();
    const minStaleAt = new Date(now.getTime() - cacheSettings.maxAge + cacheSettings.staleTTL);
    await initializeCache();
    const existing = !cacheSettings.disableCache && !skipCache ? cache.get(cacheKey) : undefined;
    if (existing && (allowStale || existing.staleAt > now) && existing.staleAt > minStaleAt) {
      // Restore from cache whether SSE is supported
      if (existing.sse) supportsSSE.add(key);

      // Reload features in the background if stale
      if (existing.staleAt < now) {
        fetchFeatures(instance);
      }
      // Otherwise, if we don't need to refresh now, start a background sync
      else {
        startAutoRefresh(instance);
      }
      return {
        data: existing.data,
        success: true,
        source: "cache"
      };
    } else {
      const res = await promiseTimeout(fetchFeatures(instance), timeout);
      return res || {
        data: null,
        success: false,
        source: "timeout",
        error: new Error("Timeout")
      };
    }
  }
  function getKey(instance) {
    const [apiHost, clientKey] = instance.getApiInfo();
    return `${apiHost}||${clientKey}`;
  }
  function getCacheKey(instance) {
    const baseKey = getKey(instance);
    if (!("isRemoteEval" in instance) || !instance.isRemoteEval()) return baseKey;
    const attributes = instance.getAttributes();
    const cacheKeyAttributes = instance.getCacheKeyAttributes() || Object.keys(instance.getAttributes());
    const ca = {};
    cacheKeyAttributes.forEach(key => {
      ca[key] = attributes[key];
    });
    const fv = instance.getForcedVariations();
    const url = instance.getUrl();
    return `${baseKey}||${JSON.stringify({
    ca,
    fv,
    url
  })}`;
  }

  // Populate cache from localStorage (if available)
  async function initializeCache() {
    if (cacheInitialized) return;
    cacheInitialized = true;
    try {
      if (polyfills.localStorage) {
        const value = await polyfills.localStorage.getItem(cacheSettings.cacheKey);
        if (!cacheSettings.disableCache && value) {
          const parsed = JSON.parse(value);
          if (parsed && Array.isArray(parsed)) {
            parsed.forEach(([key, data]) => {
              cache.set(key, {
                ...data,
                staleAt: new Date(data.staleAt)
              });
            });
          }
          cleanupCache();
        }
      }
    } catch (e) {
      // Ignore localStorage errors
    }
    if (!cacheSettings.disableIdleStreams) {
      const cleanupFn = helpers.startIdleListener();
      if (cleanupFn) {
        helpers.stopIdleListener = cleanupFn;
      }
    }
  }

  // Enforce the maxEntries limit
  function cleanupCache() {
    const entriesWithTimestamps = Array.from(cache.entries()).map(([key, value]) => ({
      key,
      staleAt: value.staleAt.getTime()
    })).sort((a, b) => a.staleAt - b.staleAt);
    const entriesToRemoveCount = Math.min(Math.max(0, cache.size - cacheSettings.maxEntries), cache.size);
    for (let i = 0; i < entriesToRemoveCount; i++) {
      cache.delete(entriesWithTimestamps[i].key);
    }
  }

  // Called whenever new features are fetched from the API
  function onNewFeatureData(key, cacheKey, data) {
    // If contents haven't changed, ignore the update, extend the stale TTL
    const version = data.dateUpdated || "";
    const staleAt = new Date(Date.now() + cacheSettings.staleTTL);
    const existing = !cacheSettings.disableCache ? cache.get(cacheKey) : undefined;
    if (existing && version && existing.version === version) {
      existing.staleAt = staleAt;
      updatePersistentCache();
      return;
    }
    if (!cacheSettings.disableCache) {
      // Update in-memory cache
      cache.set(cacheKey, {
        data,
        version,
        staleAt,
        sse: supportsSSE.has(key)
      });
      cleanupCache();
    }
    // Update local storage (don't await this, just update asynchronously)
    updatePersistentCache();

    // Update features for all subscribed GrowthBook instances
    const instances = subscribedInstances.get(key);
    instances && instances.forEach(instance => refreshInstance(instance, data));
  }
  async function refreshInstance(instance, data) {
    await instance.setPayload(data || instance.getPayload());
  }

  // Fetch the features payload from helper function or from in-mem injected payload
  async function fetchFeatures(instance) {
    const {
      apiHost,
      apiRequestHeaders
    } = instance.getApiHosts();
    const clientKey = instance.getClientKey();
    const remoteEval = "isRemoteEval" in instance && instance.isRemoteEval();
    const key = getKey(instance);
    const cacheKey = getCacheKey(instance);
    let promise = activeFetches.get(cacheKey);
    if (!promise) {
      const fetcher = remoteEval ? helpers.fetchRemoteEvalCall({
        host: apiHost,
        clientKey,
        payload: {
          attributes: instance.getAttributes(),
          forcedVariations: instance.getForcedVariations(),
          forcedFeatures: Array.from(instance.getForcedFeatures().entries()),
          url: instance.getUrl()
        },
        headers: apiRequestHeaders
      }) : helpers.fetchFeaturesCall({
        host: apiHost,
        clientKey,
        headers: apiRequestHeaders
      });

      // TODO: auto-retry if status code indicates a temporary error
      promise = fetcher.then(res => {
        if (!res.ok) {
          throw new Error(`HTTP error: ${res.status}`);
        }
        if (res.headers.get("x-sse-support") === "enabled") {
          supportsSSE.add(key);
        }
        return res.json();
      }).then(data => {
        onNewFeatureData(key, cacheKey, data);
        startAutoRefresh(instance);
        activeFetches.delete(cacheKey);
        return {
          data,
          success: true,
          source: "network"
        };
      }).catch(e => {
        activeFetches.delete(cacheKey);
        return {
          data: null,
          source: "error",
          success: false,
          error: e
        };
      });
      activeFetches.set(cacheKey, promise);
    }
    return promise;
  }

  // Start SSE streaming, listens to feature payload changes and triggers a refresh or re-fetch
  function startAutoRefresh(instance, forceSSE = false) {
    const key = getKey(instance);
    const cacheKey = getCacheKey(instance);
    const {
      streamingHost,
      streamingHostRequestHeaders
    } = instance.getApiHosts();
    const clientKey = instance.getClientKey();
    if (forceSSE) {
      supportsSSE.add(key);
    }
    if (cacheSettings.backgroundSync && supportsSSE.has(key) && polyfills.EventSource) {
      if (streams.has(key)) return;
      const channel = {
        src: null,
        host: streamingHost,
        clientKey,
        headers: streamingHostRequestHeaders,
        cb: event => {
          try {
            if (event.type === "features-updated") {
              const instances = subscribedInstances.get(key);
              instances && instances.forEach(instance => {
                fetchFeatures(instance);
              });
            } else if (event.type === "features") {
              const json = JSON.parse(event.data);
              onNewFeatureData(key, cacheKey, json);
            }
            // Reset error count on success
            channel.errors = 0;
          } catch (e) {
            onSSEError(channel);
          }
        },
        errors: 0,
        state: "active"
      };
      streams.set(key, channel);
      enableChannel(channel);
    }
  }
  function onSSEError(channel) {
    if (channel.state === "idle") return;
    channel.errors++;
    if (channel.errors > 3 || channel.src && channel.src.readyState === 2) {
      // exponential backoff after 4 errors, with jitter
      const delay = Math.pow(3, channel.errors - 3) * (1000 + Math.random() * 1000);
      disableChannel(channel);
      setTimeout(() => {
        if (["idle", "active"].includes(channel.state)) return;
        enableChannel(channel);
      }, Math.min(delay, 300000)); // 5 minutes max
    }
  }
  function disableChannel(channel) {
    if (!channel.src) return;
    channel.src.onopen = null;
    channel.src.onerror = null;
    channel.src.close();
    channel.src = null;
    if (channel.state === "active") {
      channel.state = "disabled";
    }
  }
  function enableChannel(channel) {
    channel.src = helpers.eventSourceCall({
      host: channel.host,
      clientKey: channel.clientKey,
      headers: channel.headers
    });
    channel.state = "active";
    channel.src.addEventListener("features", channel.cb);
    channel.src.addEventListener("features-updated", channel.cb);
    channel.src.onerror = () => onSSEError(channel);
    channel.src.onopen = () => {
      channel.errors = 0;
    };
  }
  function destroyChannel(channel, key) {
    disableChannel(channel);
    streams.delete(key);
  }
  function clearAutoRefresh() {
    // Clear list of which keys are auto-updated
    supportsSSE.clear();

    // Stop listening for any SSE events
    streams.forEach(destroyChannel);

    // Remove all references to GrowthBook instances
    subscribedInstances.clear();

    // Run the idle stream cleanup function
    helpers.stopIdleListener();
  }
  function startStreaming(instance, options) {
    if (options.streaming) {
      if (!instance.getClientKey()) {
        throw new Error("Must specify clientKey to enable streaming");
      }
      if (options.payload) {
        startAutoRefresh(instance, true);
      }
      subscribe(instance);
    }
  }

  /* eslint-disable @typescript-eslint/no-explicit-any */

  const _regexCache = {};

  // The top-level condition evaluation function
  function evalCondition(obj, condition,
  // Must be included for `condition` to correctly evaluate group Operators
  savedGroups) {
    savedGroups = savedGroups || {};
    // Condition is an object, keys are either specific operators or object paths
    // values are either arguments for operators or conditions for paths
    for (const [k, v] of Object.entries(condition)) {
      switch (k) {
        case "$or":
          if (!evalOr(obj, v, savedGroups)) return false;
          break;
        case "$nor":
          if (evalOr(obj, v, savedGroups)) return false;
          break;
        case "$and":
          if (!evalAnd(obj, v, savedGroups)) return false;
          break;
        case "$not":
          if (evalCondition(obj, v, savedGroups)) return false;
          break;
        default:
          if (!evalConditionValue(v, getPath(obj, k), savedGroups)) return false;
      }
    }
    return true;
  }

  // Return value at dot-separated path of an object
  function getPath(obj, path) {
    const parts = path.split(".");
    let current = obj;
    for (let i = 0; i < parts.length; i++) {
      if (current && typeof current === "object" && parts[i] in current) {
        current = current[parts[i]];
      } else {
        return null;
      }
    }
    return current;
  }

  // Transform a regex string into a real RegExp object
  function getRegex(regex, insensitive = false) {
    const cacheKey = `${regex}${insensitive ? "/i" : ""}`;
    if (!_regexCache[cacheKey]) {
      _regexCache[cacheKey] = new RegExp(regex.replace(/([^\\])\//g, "$1\\/"), insensitive ? "i" : undefined);
    }
    return _regexCache[cacheKey];
  }

  // Evaluate a single value against a condition
  function evalConditionValue(condition, value, savedGroups, insensitive = false) {
    // Simple equality comparisons
    if (typeof condition === "string") {
      if (insensitive) {
        return String(value).toLowerCase() === condition.toLowerCase();
      }
      return value + "" === condition;
    }
    if (typeof condition === "number") {
      return value * 1 === condition;
    }
    if (typeof condition === "boolean") {
      return value !== null && !!value === condition;
    }
    if (condition === null) {
      return value === null;
    }
    if (Array.isArray(condition) || !isOperatorObject(condition)) {
      return JSON.stringify(value) === JSON.stringify(condition);
    }

    // This is a special operator condition and we should evaluate each one separately
    for (const op in condition) {
      if (!evalOperatorCondition(op, value, condition[op], savedGroups)) {
        return false;
      }
    }
    return true;
  }

  // If the object has only keys that start with '$'
  function isOperatorObject(obj) {
    const keys = Object.keys(obj);
    return keys.length > 0 && keys.filter(k => k[0] === "$").length === keys.length;
  }

  // Return the data type of a value
  function getType(v) {
    if (v === null) return "null";
    if (Array.isArray(v)) return "array";
    const t = typeof v;
    if (["string", "number", "boolean", "object", "undefined"].includes(t)) {
      return t;
    }
    return "unknown";
  }

  // At least one element of actual must match the expected condition/value
  function elemMatch(actual, expected, savedGroups) {
    if (!Array.isArray(actual)) return false;
    const check = isOperatorObject(expected) ? v => evalConditionValue(expected, v, savedGroups) : v => evalCondition(v, expected, savedGroups);
    for (let i = 0; i < actual.length; i++) {
      if (actual[i] && check(actual[i])) {
        return true;
      }
    }
    return false;
  }
  function isIn(actual, expected, insensitive = false) {
    if (insensitive) {
      const caseFold = val => typeof val === "string" ? val.toLowerCase() : val;
      // Do an intersection if attribute is an array (insensitive)
      if (Array.isArray(actual)) {
        return actual.some(el => expected.some(exp => caseFold(el) === caseFold(exp)));
      }
      return expected.some(exp => caseFold(actual) === caseFold(exp));
    }
    // Do an intersection if attribute is an array
    if (Array.isArray(actual)) {
      return actual.some(el => expected.includes(el));
    }
    return expected.includes(actual);
  }
  function isInAll(actual, expected, savedGroups, insensitive = false) {
    if (!Array.isArray(actual)) return false;
    for (let i = 0; i < expected.length; i++) {
      let passed = false;
      for (let j = 0; j < actual.length; j++) {
        if (evalConditionValue(expected[i], actual[j], savedGroups, insensitive)) {
          passed = true;
          break;
        }
      }
      if (!passed) return false;
    }
    return true;
  }

  // Evaluate a single operator condition
  function evalOperatorCondition(operator, actual, expected, savedGroups) {
    switch (operator) {
      case "$veq":
        return paddedVersionString(actual) === paddedVersionString(expected);
      case "$vne":
        return paddedVersionString(actual) !== paddedVersionString(expected);
      case "$vgt":
        return paddedVersionString(actual) > paddedVersionString(expected);
      case "$vgte":
        return paddedVersionString(actual) >= paddedVersionString(expected);
      case "$vlt":
        return paddedVersionString(actual) < paddedVersionString(expected);
      case "$vlte":
        return paddedVersionString(actual) <= paddedVersionString(expected);
      case "$eq":
        return actual === expected;
      case "$ne":
        return actual !== expected;
      case "$lt":
        return actual < expected;
      case "$lte":
        return actual <= expected;
      case "$gt":
        return actual > expected;
      case "$gte":
        return actual >= expected;
      case "$exists":
        // Using `!=` and `==` instead of strict checks so it also matches for undefined
        return expected ? actual != null : actual == null;
      case "$in":
        if (!Array.isArray(expected)) return false;
        return isIn(actual, expected);
      case "$ini":
        if (!Array.isArray(expected)) return false;
        return isIn(actual, expected, true);
      case "$inGroup":
        return isIn(actual, savedGroups[expected] || []);
      case "$notInGroup":
        return !isIn(actual, savedGroups[expected] || []);
      case "$nin":
        if (!Array.isArray(expected)) return false;
        return !isIn(actual, expected);
      case "$nini":
        if (!Array.isArray(expected)) return false;
        return !isIn(actual, expected, true);
      case "$not":
        return !evalConditionValue(expected, actual, savedGroups);
      case "$size":
        if (!Array.isArray(actual)) return false;
        return evalConditionValue(expected, actual.length, savedGroups);
      case "$elemMatch":
        return elemMatch(actual, expected, savedGroups);
      case "$all":
        if (!Array.isArray(expected)) return false;
        return isInAll(actual, expected, savedGroups);
      case "$alli":
        if (!Array.isArray(expected)) return false;
        return isInAll(actual, expected, savedGroups, true);
      case "$regex":
        try {
          return getRegex(expected).test(actual);
        } catch (e) {
          return false;
        }
      case "$regexi":
        try {
          return getRegex(expected, true).test(actual);
        } catch (e) {
          return false;
        }
      case "$type":
        return getType(actual) === expected;
      default:
        console.error("Unknown operator: " + operator);
        return false;
    }
  }

  // Recursive $or rule
  function evalOr(obj, conditions, savedGroups) {
    if (!conditions.length) return true;
    for (let i = 0; i < conditions.length; i++) {
      if (evalCondition(obj, conditions[i], savedGroups)) {
        return true;
      }
    }
    return false;
  }

  // Recursive $and rule
  function evalAnd(obj, conditions, savedGroups) {
    for (let i = 0; i < conditions.length; i++) {
      if (!evalCondition(obj, conditions[i], savedGroups)) {
        return false;
      }
    }
    return true;
  }

  const EVENT_FEATURE_EVALUATED = "Feature Evaluated";
  const EVENT_EXPERIMENT_VIEWED = "Experiment Viewed";
  function getForcedFeatureValues(ctx) {
    // Merge user and global values
    const ret = new Map();
    if (ctx.global.forcedFeatureValues) {
      ctx.global.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
    }
    if (ctx.user.forcedFeatureValues) {
      ctx.user.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
    }
    return ret;
  }
  function getForcedVariations(ctx) {
    // Merge user and global values
    if (ctx.global.forcedVariations && ctx.user.forcedVariations) {
      return {
        ...ctx.global.forcedVariations,
        ...ctx.user.forcedVariations
      };
    } else if (ctx.global.forcedVariations) {
      return ctx.global.forcedVariations;
    } else if (ctx.user.forcedVariations) {
      return ctx.user.forcedVariations;
    } else {
      return {};
    }
  }
  async function safeCall(fn) {
    try {
      await fn();
    } catch (e) {
      // Do nothing
    }
  }
  function onExperimentViewed(ctx, experiment, result) {
    // Make sure a tracking callback is only fired once per unique experiment
    if (ctx.user.trackedExperiments) {
      const k = getExperimentDedupeKey(experiment, result);
      if (ctx.user.trackedExperiments.has(k)) {
        return [];
      }
      ctx.user.trackedExperiments.add(k);
    }
    if (ctx.user.enableDevMode && ctx.user.devLogs) {
      ctx.user.devLogs.push({
        experiment,
        result,
        timestamp: Date.now().toString(),
        logType: "experiment"
      });
    }
    const calls = [];
    if (ctx.global.trackingCallback) {
      const cb = ctx.global.trackingCallback;
      calls.push(safeCall(() => cb(experiment, result, ctx.user)));
    }
    if (ctx.user.trackingCallback) {
      const cb = ctx.user.trackingCallback;
      calls.push(safeCall(() => cb(experiment, result)));
    }
    if (ctx.global.eventLogger) {
      const cb = ctx.global.eventLogger;
      calls.push(safeCall(() => cb(EVENT_EXPERIMENT_VIEWED, {
        experimentId: experiment.key,
        variationId: result.key,
        hashAttribute: result.hashAttribute,
        hashValue: result.hashValue
      }, ctx.user)));
    }
    return calls;
  }
  function onFeatureUsage(ctx, key, ret) {
    // Only track a feature once, unless the assigned value changed
    if (ctx.user.trackedFeatureUsage) {
      const stringifiedValue = JSON.stringify(ret.value);
      if (ctx.user.trackedFeatureUsage[key] === stringifiedValue) return;
      ctx.user.trackedFeatureUsage[key] = stringifiedValue;
      if (ctx.user.enableDevMode && ctx.user.devLogs) {
        ctx.user.devLogs.push({
          featureKey: key,
          result: ret,
          timestamp: Date.now().toString(),
          logType: "feature"
        });
      }
    }
    if (ctx.global.onFeatureUsage) {
      const cb = ctx.global.onFeatureUsage;
      safeCall(() => cb(key, ret, ctx.user));
    }
    if (ctx.user.onFeatureUsage) {
      const cb = ctx.user.onFeatureUsage;
      safeCall(() => cb(key, ret));
    }
    if (ctx.global.eventLogger) {
      const cb = ctx.global.eventLogger;
      safeCall(() => cb(EVENT_FEATURE_EVALUATED, {
        feature: key,
        source: ret.source,
        value: ret.value,
        ruleId: ret.source === "defaultValue" ? "$default" : ret.ruleId || "",
        variationId: ret.experimentResult ? ret.experimentResult.key : ""
      }, ctx.user));
    }
  }
  function evalFeature(id, ctx) {
    if (ctx.stack.evaluatedFeatures.has(id)) {
      return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
    }
    ctx.stack.evaluatedFeatures.add(id);
    ctx.stack.id = id;

    // Global override
    const forcedValues = getForcedFeatureValues(ctx);
    if (forcedValues.has(id)) {
      return getFeatureResult(ctx, id, forcedValues.get(id), "override");
    }

    // Unknown feature id
    if (!ctx.global.features || !ctx.global.features[id]) {
      return getFeatureResult(ctx, id, null, "unknownFeature");
    }

    // Get the feature
    const feature = ctx.global.features[id];

    // Loop through the rules
    if (feature.rules) {
      const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
      rules: for (const rule of feature.rules) {
        // If there are prerequisite flag(s), evaluate them
        if (rule.parentConditions) {
          for (const parentCondition of rule.parentConditions) {
            ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
            const parentResult = evalFeature(parentCondition.id, ctx);
            // break out for cyclic prerequisites
            if (parentResult.source === "cyclicPrerequisite") {
              return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
            }
            const evalObj = {
              value: parentResult.value
            };
            const evaled = evalCondition(evalObj, parentCondition.condition || {});
            if (!evaled) {
              // blocking prerequisite eval failed: feature evaluation fails
              if (parentCondition.gate) {
                return getFeatureResult(ctx, id, null, "prerequisite");
              }
              continue rules;
            }
          }
        }

        // If there are filters for who is included (e.g. namespaces)
        if (rule.filters && isFilteredOut(rule.filters, ctx)) {
          continue;
        }

        // Feature value is being forced
        if ("force" in rule) {
          // If it's a conditional rule, skip if the condition doesn't pass
          if (rule.condition && !conditionPasses(rule.condition, ctx)) {
            continue;
          }

          // If this is a percentage rollout, skip if not included
          if (!isIncludedInRollout(ctx, rule.seed || id, rule.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !rule.disableStickyBucketing ? rule.fallbackAttribute : undefined, rule.range, rule.coverage, rule.hashVersion)) {
            continue;
          }

          // If this was a remotely evaluated experiment, fire the tracking callbacks
          if (rule.tracks) {
            rule.tracks.forEach(t => {
              const calls = onExperimentViewed(ctx, t.experiment, t.result);
              if (!calls.length && ctx.global.saveDeferredTrack) {
                ctx.global.saveDeferredTrack({
                  experiment: t.experiment,
                  result: t.result
                });
              }
            });
          }
          return getFeatureResult(ctx, id, rule.force, "force", rule.id);
        }
        if (!rule.variations) {
          continue;
        }

        // For experiment rules, run an experiment
        const exp = {
          variations: rule.variations,
          key: rule.key || id
        };
        if ("coverage" in rule) exp.coverage = rule.coverage;
        if (rule.weights) exp.weights = rule.weights;
        if (rule.hashAttribute) exp.hashAttribute = rule.hashAttribute;
        if (rule.fallbackAttribute) exp.fallbackAttribute = rule.fallbackAttribute;
        if (rule.disableStickyBucketing) exp.disableStickyBucketing = rule.disableStickyBucketing;
        if (rule.bucketVersion !== undefined) exp.bucketVersion = rule.bucketVersion;
        if (rule.minBucketVersion !== undefined) exp.minBucketVersion = rule.minBucketVersion;
        if (rule.namespace) exp.namespace = rule.namespace;
        if (rule.meta) exp.meta = rule.meta;
        if (rule.ranges) exp.ranges = rule.ranges;
        if (rule.name) exp.name = rule.name;
        if (rule.phase) exp.phase = rule.phase;
        if (rule.seed) exp.seed = rule.seed;
        if (rule.hashVersion) exp.hashVersion = rule.hashVersion;
        if (rule.filters) exp.filters = rule.filters;
        if (rule.condition) exp.condition = rule.condition;

        // Only return a value if the user is part of the experiment
        const {
          result
        } = runExperiment(exp, id, ctx);
        ctx.global.onExperimentEval && ctx.global.onExperimentEval(exp, result);
        if (result.inExperiment && !result.passthrough) {
          return getFeatureResult(ctx, id, result.value, "experiment", rule.id, exp, result);
        }
      }
    }

    // Fall back to using the default value
    return getFeatureResult(ctx, id, feature.defaultValue === undefined ? null : feature.defaultValue, "defaultValue");
  }
  function runExperiment(experiment, featureId, ctx) {
    const key = experiment.key;
    const numVariations = experiment.variations.length;

    // 1. If experiment has less than 2 variations, return immediately
    if (numVariations < 2) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 2. If the context is disabled, return immediately
    if (ctx.global.enabled === false || ctx.user.enabled === false) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 2.5. Merge in experiment overrides from the context
    experiment = mergeOverrides(experiment, ctx);

    // 2.6 New, more powerful URL targeting
    if (experiment.urlPatterns && !isURLTargeted(ctx.user.url || "", experiment.urlPatterns)) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 3. If a variation is forced from a querystring, return the forced variation
    const qsOverride = getQueryStringOverride(key, ctx.user.url || "", numVariations);
    if (qsOverride !== null) {
      return {
        result: getExperimentResult(ctx, experiment, qsOverride, false, featureId)
      };
    }

    // 4. If a variation is forced in the context, return the forced variation
    const forcedVariations = getForcedVariations(ctx);
    if (key in forcedVariations) {
      const variation = forcedVariations[key];
      return {
        result: getExperimentResult(ctx, experiment, variation, false, featureId)
      };
    }

    // 5. Exclude if a draft experiment or not active
    if (experiment.status === "draft" || experiment.active === false) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 6. Get the hash attribute and return if empty
    const {
      hashAttribute,
      hashValue
    } = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
    if (!hashValue) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }
    let assigned = -1;
    let foundStickyBucket = false;
    let stickyBucketVersionIsBlocked = false;
    if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
      const {
        variation,
        versionIsBlocked
      } = getStickyBucketVariation({
        ctx,
        expKey: experiment.key,
        expBucketVersion: experiment.bucketVersion,
        expHashAttribute: experiment.hashAttribute,
        expFallbackAttribute: experiment.fallbackAttribute,
        expMinBucketVersion: experiment.minBucketVersion,
        expMeta: experiment.meta
      });
      foundStickyBucket = variation >= 0;
      assigned = variation;
      stickyBucketVersionIsBlocked = !!versionIsBlocked;
    }

    // Some checks are not needed if we already have a sticky bucket
    if (!foundStickyBucket) {
      // 7. Exclude if user is filtered out (used to be called "namespace")
      if (experiment.filters) {
        if (isFilteredOut(experiment.filters, ctx)) {
          return {
            result: getExperimentResult(ctx, experiment, -1, false, featureId)
          };
        }
      } else if (experiment.namespace && !inNamespace(hashValue, experiment.namespace)) {
        return {
          result: getExperimentResult(ctx, experiment, -1, false, featureId)
        };
      }

      // 7.5. Exclude if experiment.include returns false or throws
      if (experiment.include && !isIncluded(experiment.include)) {
        return {
          result: getExperimentResult(ctx, experiment, -1, false, featureId)
        };
      }

      // 8. Exclude if condition is false
      if (experiment.condition && !conditionPasses(experiment.condition, ctx)) {
        return {
          result: getExperimentResult(ctx, experiment, -1, false, featureId)
        };
      }

      // 8.05. Exclude if prerequisites are not met
      if (experiment.parentConditions) {
        const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
        for (const parentCondition of experiment.parentConditions) {
          ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
          const parentResult = evalFeature(parentCondition.id, ctx);
          // break out for cyclic prerequisites
          if (parentResult.source === "cyclicPrerequisite") {
            return {
              result: getExperimentResult(ctx, experiment, -1, false, featureId)
            };
          }
          const evalObj = {
            value: parentResult.value
          };
          if (!evalCondition(evalObj, parentCondition.condition || {})) {
            return {
              result: getExperimentResult(ctx, experiment, -1, false, featureId)
            };
          }
        }
      }

      // 8.1. Exclude if user is not in a required group
      if (experiment.groups && !hasGroupOverlap(experiment.groups, ctx)) {
        return {
          result: getExperimentResult(ctx, experiment, -1, false, featureId)
        };
      }
    }

    // 8.2. Old style URL targeting
    if (experiment.url && !urlIsValid(experiment.url, ctx)) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 9. Get the variation from the sticky bucket or get bucket ranges and choose variation
    const n = hash(experiment.seed || key, hashValue, experiment.hashVersion || 1);
    if (n === null) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }
    if (!foundStickyBucket) {
      const ranges = experiment.ranges || getBucketRanges(numVariations, experiment.coverage === undefined ? 1 : experiment.coverage, experiment.weights);
      assigned = chooseVariation(n, ranges);
    }

    // 9.5 Unenroll if any prior sticky buckets are blocked by version
    if (stickyBucketVersionIsBlocked) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId, undefined, true)
      };
    }

    // 10. Return if not in experiment
    if (assigned < 0) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 11. Experiment has a forced variation
    if ("force" in experiment) {
      return {
        result: getExperimentResult(ctx, experiment, experiment.force === undefined ? -1 : experiment.force, false, featureId)
      };
    }

    // 12. Exclude if in QA mode
    if (ctx.global.qaMode || ctx.user.qaMode) {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 12.5. Exclude if experiment is stopped
    if (experiment.status === "stopped") {
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 13. Build the result object
    const result = getExperimentResult(ctx, experiment, assigned, true, featureId, n, foundStickyBucket);

    // 13.5. Persist sticky bucket
    if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
      const {
        changed,
        key: attrKey,
        doc
      } = generateStickyBucketAssignmentDoc(ctx, hashAttribute, toString(hashValue), {
        [getStickyBucketExperimentKey(experiment.key, experiment.bucketVersion)]: result.key
      });
      if (changed) {
        // update local docs
        ctx.user.stickyBucketAssignmentDocs = ctx.user.stickyBucketAssignmentDocs || {};
        ctx.user.stickyBucketAssignmentDocs[attrKey] = doc;
        // save doc
        ctx.user.saveStickyBucketAssignmentDoc(doc);
      }
    }

    // 14. Fire the tracking callback(s)
    // Store the promise in case we're awaiting it (ex: browser url redirects)
    const trackingCalls = onExperimentViewed(ctx, experiment, result);
    if (trackingCalls.length === 0 && ctx.global.saveDeferredTrack) {
      ctx.global.saveDeferredTrack({
        experiment,
        result
      });
    }
    const trackingCall = !trackingCalls.length ? undefined : trackingCalls.length === 1 ? trackingCalls[0] : Promise.all(trackingCalls).then(() => {});

    // 14.1 Keep track of completed changeIds
    "changeId" in experiment && experiment.changeId && ctx.global.recordChangeId && ctx.global.recordChangeId(experiment.changeId);
    return {
      result,
      trackingCall
    };
  }
  function getFeatureResult(ctx, key, value, source, ruleId, experiment, result) {
    const ret = {
      value,
      on: !!value,
      off: !value,
      source,
      ruleId: ruleId || ""
    };
    if (experiment) ret.experiment = experiment;
    if (result) ret.experimentResult = result;

    // Track the usage of this feature in real-time
    if (source !== "override") {
      onFeatureUsage(ctx, key, ret);
    }
    return ret;
  }
  function getAttributes(ctx) {
    return {
      ...ctx.user.attributes,
      ...ctx.user.attributeOverrides
    };
  }
  function conditionPasses(condition, ctx) {
    return evalCondition(getAttributes(ctx), condition, ctx.global.savedGroups || {});
  }
  function isFilteredOut(filters, ctx) {
    return filters.some(filter => {
      const {
        hashValue
      } = getHashAttribute(ctx, filter.attribute);
      if (!hashValue) return true;
      const n = hash(filter.seed, hashValue, filter.hashVersion || 2);
      if (n === null) return true;
      return !filter.ranges.some(r => inRange(n, r));
    });
  }
  function isIncludedInRollout(ctx, seed, hashAttribute, fallbackAttribute, range, coverage, hashVersion) {
    if (!range && coverage === undefined) return true;
    if (!range && coverage === 0) return false;
    const {
      hashValue
    } = getHashAttribute(ctx, hashAttribute, fallbackAttribute);
    if (!hashValue) {
      return false;
    }
    const n = hash(seed, hashValue, hashVersion || 1);
    if (n === null) return false;
    return range ? inRange(n, range) : coverage !== undefined ? n <= coverage : true;
  }
  function getExperimentResult(ctx, experiment, variationIndex, hashUsed, featureId, bucket, stickyBucketUsed) {
    let inExperiment = true;
    // If assigned variation is not valid, use the baseline and mark the user as not in the experiment
    if (variationIndex < 0 || variationIndex >= experiment.variations.length) {
      variationIndex = 0;
      inExperiment = false;
    }
    const {
      hashAttribute,
      hashValue
    } = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
    const meta = experiment.meta ? experiment.meta[variationIndex] : {};
    const res = {
      key: meta.key || "" + variationIndex,
      featureId,
      inExperiment,
      hashUsed,
      variationId: variationIndex,
      value: experiment.variations[variationIndex],
      hashAttribute,
      hashValue,
      stickyBucketUsed: !!stickyBucketUsed
    };
    if (meta.name) res.name = meta.name;
    if (bucket !== undefined) res.bucket = bucket;
    if (meta.passthrough) res.passthrough = meta.passthrough;
    return res;
  }
  function mergeOverrides(experiment, ctx) {
    const key = experiment.key;
    const o = ctx.global.overrides;
    if (o && o[key]) {
      experiment = Object.assign({}, experiment, o[key]);
      if (typeof experiment.url === "string") {
        experiment.url = getUrlRegExp(
        // eslint-disable-next-line
        experiment.url);
      }
    }
    return experiment;
  }
  function getHashAttribute(ctx, attr, fallback) {
    let hashAttribute = attr || "id";
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    let hashValue = "";
    const attributes = getAttributes(ctx);
    if (attributes[hashAttribute]) {
      hashValue = attributes[hashAttribute];
    }

    // if no match, try fallback
    if (!hashValue && fallback) {
      if (attributes[fallback]) {
        hashValue = attributes[fallback];
      }
      if (hashValue) {
        hashAttribute = fallback;
      }
    }
    return {
      hashAttribute,
      hashValue
    };
  }
  function urlIsValid(urlRegex, ctx) {
    const url = ctx.user.url;
    if (!url) return false;
    const pathOnly = url.replace(/^https?:\/\//, "").replace(/^[^/]*\//, "/");
    if (urlRegex.test(url)) return true;
    if (urlRegex.test(pathOnly)) return true;
    return false;
  }
  function hasGroupOverlap(expGroups, ctx) {
    const groups = ctx.global.groups || {};
    for (let i = 0; i < expGroups.length; i++) {
      if (groups[expGroups[i]]) return true;
    }
    return false;
  }
  function getStickyBucketVariation({
    ctx,
    expKey,
    expBucketVersion,
    expHashAttribute,
    expFallbackAttribute,
    expMinBucketVersion,
    expMeta
  }) {
    expBucketVersion = expBucketVersion || 0;
    expMinBucketVersion = expMinBucketVersion || 0;
    expHashAttribute = expHashAttribute || "id";
    expMeta = expMeta || [];
    const id = getStickyBucketExperimentKey(expKey, expBucketVersion);
    const assignments = getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute);

    // users with any blocked bucket version (0 to minExperimentBucketVersion - 1) are excluded from the test
    if (expMinBucketVersion > 0) {
      for (let i = 0; i < expMinBucketVersion; i++) {
        const blockedKey = getStickyBucketExperimentKey(expKey, i);
        if (assignments[blockedKey] !== undefined) {
          return {
            variation: -1,
            versionIsBlocked: true
          };
        }
      }
    }
    const variationKey = assignments[id];
    if (variationKey === undefined)
      // no assignment found
      return {
        variation: -1
      };
    const variation = expMeta.findIndex(m => m.key === variationKey);
    if (variation < 0)
      // invalid assignment, treat as "no assignment found"
      return {
        variation: -1
      };
    return {
      variation
    };
  }
  function getStickyBucketExperimentKey(experimentKey, experimentBucketVersion) {
    experimentBucketVersion = experimentBucketVersion || 0;
    return `${experimentKey}__${experimentBucketVersion}`;
  }
  function getStickyBucketAttributeKey(attributeName, attributeValue) {
    return `${attributeName}||${attributeValue}`;
  }
  function getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute) {
    if (!ctx.user.stickyBucketAssignmentDocs) return {};
    const {
      hashAttribute,
      hashValue
    } = getHashAttribute(ctx, expHashAttribute);
    const hashKey = getStickyBucketAttributeKey(hashAttribute, toString(hashValue));
    const {
      hashAttribute: fallbackAttribute,
      hashValue: fallbackValue
    } = getHashAttribute(ctx, expFallbackAttribute);
    const fallbackKey = fallbackValue ? getStickyBucketAttributeKey(fallbackAttribute, toString(fallbackValue)) : null;
    const assignments = {};
    if (fallbackKey && ctx.user.stickyBucketAssignmentDocs[fallbackKey]) {
      Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[fallbackKey].assignments || {});
    }
    if (ctx.user.stickyBucketAssignmentDocs[hashKey]) {
      Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[hashKey].assignments || {});
    }
    return assignments;
  }
  function generateStickyBucketAssignmentDoc(ctx, attributeName, attributeValue, assignments) {
    const key = getStickyBucketAttributeKey(attributeName, attributeValue);
    const existingAssignments = ctx.user.stickyBucketAssignmentDocs && ctx.user.stickyBucketAssignmentDocs[key] ? ctx.user.stickyBucketAssignmentDocs[key].assignments || {} : {};
    const newAssignments = {
      ...existingAssignments,
      ...assignments
    };
    const changed = JSON.stringify(existingAssignments) !== JSON.stringify(newAssignments);
    return {
      key,
      doc: {
        attributeName,
        attributeValue,
        assignments: newAssignments
      },
      changed
    };
  }
  function deriveStickyBucketIdentifierAttributes(ctx, data) {
    const attributes = new Set();
    const features = data && data.features ? data.features : ctx.global.features || {};
    const experiments = data && data.experiments ? data.experiments : ctx.global.experiments || [];
    Object.keys(features).forEach(id => {
      const feature = features[id];
      if (feature.rules) {
        for (const rule of feature.rules) {
          if (rule.variations) {
            attributes.add(rule.hashAttribute || "id");
            if (rule.fallbackAttribute) {
              attributes.add(rule.fallbackAttribute);
            }
          }
        }
      }
    });
    experiments.map(experiment => {
      attributes.add(experiment.hashAttribute || "id");
      if (experiment.fallbackAttribute) {
        attributes.add(experiment.fallbackAttribute);
      }
    });
    return Array.from(attributes);
  }
  async function getAllStickyBucketAssignmentDocs(ctx, stickyBucketService, data) {
    const attributes = getStickyBucketAttributes(ctx, data);
    return stickyBucketService.getAllAssignments(attributes);
  }
  function getStickyBucketAttributes(ctx, data) {
    const attributes = {};
    const stickyBucketIdentifierAttributes = deriveStickyBucketIdentifierAttributes(ctx, data);
    stickyBucketIdentifierAttributes.forEach(attr => {
      const {
        hashValue
      } = getHashAttribute(ctx, attr);
      attributes[attr] = toString(hashValue);
    });
    return attributes;
  }
  async function decryptPayload(data, decryptionKey, subtle) {
    data = {
      ...data
    };
    if (data.encryptedFeatures) {
      try {
        data.features = JSON.parse(await decrypt(data.encryptedFeatures, decryptionKey, subtle));
      } catch (e) {
        console.error(e);
      }
      delete data.encryptedFeatures;
    }
    if (data.encryptedExperiments) {
      try {
        data.experiments = JSON.parse(await decrypt(data.encryptedExperiments, decryptionKey, subtle));
      } catch (e) {
        console.error(e);
      }
      delete data.encryptedExperiments;
    }
    if (data.encryptedSavedGroups) {
      try {
        data.savedGroups = JSON.parse(await decrypt(data.encryptedSavedGroups, decryptionKey, subtle));
      } catch (e) {
        console.error(e);
      }
      delete data.encryptedSavedGroups;
    }
    return data;
  }
  function getApiHosts(options) {
    const defaultHost = options.apiHost || "https://cdn.growthbook.io";
    return {
      apiHost: defaultHost.replace(/\/*$/, ""),
      streamingHost: (options.streamingHost || defaultHost).replace(/\/*$/, ""),
      apiRequestHeaders: options.apiHostRequestHeaders,
      streamingHostRequestHeaders: options.streamingHostRequestHeaders
    };
  }
  function getExperimentDedupeKey(experiment, result) {
    return result.hashAttribute + result.hashValue + experiment.key + result.variationId;
  }

  const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
  const SDK_VERSION$1 = loadSDKVersion();
  class GrowthBook {
    // context is technically private, but some tools depend on it so we can't mangle the name

    // Properties and methods that start with "_" are mangled by Terser (saves ~150 bytes)

    constructor(options) {
      options = options || {};
      // These properties are all initialized in the constructor instead of above
      // This saves ~80 bytes in the final output
      this.version = SDK_VERSION$1;
      this._options = this.context = options;
      this._renderer = options.renderer || null;
      this._trackedExperiments = new Set();
      this._completedChangeIds = new Set();
      this._trackedFeatures = {};
      this.debug = !!options.debug;
      this._subscriptions = new Set();
      this.ready = false;
      this._assigned = new Map();
      this._activeAutoExperiments = new Map();
      this._triggeredExpKeys = new Set();
      this._initialized = false;
      this._redirectedUrl = "";
      this._deferredTrackingCalls = new Map();
      this._autoExperimentsAllowed = !options.disableExperimentsOnLoad;
      this._destroyCallbacks = [];
      this.logs = [];
      this.log = this.log.bind(this);
      this._saveDeferredTrack = this._saveDeferredTrack.bind(this);
      this._onExperimentEval = this._onExperimentEval.bind(this);
      this._fireSubscriptions = this._fireSubscriptions.bind(this);
      this._recordChangedId = this._recordChangedId.bind(this);
      if (options.remoteEval) {
        if (options.decryptionKey) {
          throw new Error("Encryption is not available for remoteEval");
        }
        if (!options.clientKey) {
          throw new Error("Missing clientKey");
        }
        let isGbHost = false;
        try {
          isGbHost = !!new URL(options.apiHost || "").hostname.match(/growthbook\.io$/i);
        } catch (e) {
          // ignore invalid URLs
        }
        if (isGbHost) {
          throw new Error("Cannot use remoteEval on GrowthBook Cloud");
        }
      } else {
        if (options.cacheKeyAttributes) {
          throw new Error("cacheKeyAttributes are only used for remoteEval");
        }
      }
      if (options.stickyBucketService) {
        const s = options.stickyBucketService;
        this._saveStickyBucketAssignmentDoc = doc => {
          return s.saveAssignments(doc);
        };
      }
      if (options.plugins) {
        for (const plugin of options.plugins) {
          plugin(this);
        }
      }
      if (options.features) {
        this.ready = true;
      }
      if (isBrowser && options.enableDevMode) {
        window._growthbook = this;
        document.dispatchEvent(new Event("gbloaded"));
      }
      if (options.experiments) {
        this.ready = true;
        this._updateAllAutoExperiments();
      }

      // Hydrate sticky bucket service
      if (this._options.stickyBucketService && this._options.stickyBucketAssignmentDocs) {
        for (const key in this._options.stickyBucketAssignmentDocs) {
          const doc = this._options.stickyBucketAssignmentDocs[key];
          if (doc) {
            this._options.stickyBucketService.saveAssignments(doc).catch(() => {
              // Ignore hydration errors
            });
          }
        }
      }

      // Legacy - passing in features/experiments into the constructor instead of using init
      if (this.ready) {
        this.refreshStickyBuckets(this.getPayload());
      }
    }
    async setPayload(payload) {
      this._payload = payload;
      const data = await decryptPayload(payload, this._options.decryptionKey);
      this._decryptedPayload = data;
      await this.refreshStickyBuckets(data);
      if (data.features) {
        this._options.features = data.features;
      }
      if (data.savedGroups) {
        this._options.savedGroups = data.savedGroups;
      }
      if (data.experiments) {
        this._options.experiments = data.experiments;
        this._updateAllAutoExperiments();
      }
      this.ready = true;
      this._render();
    }
    initSync(options) {
      this._initialized = true;
      const payload = options.payload;
      if (payload.encryptedExperiments || payload.encryptedFeatures) {
        throw new Error("initSync does not support encrypted payloads");
      }
      if (this._options.stickyBucketService && !this._options.stickyBucketAssignmentDocs) {
        this._options.stickyBucketAssignmentDocs = this.generateStickyBucketAssignmentDocsSync(this._options.stickyBucketService, payload);
      }
      this._payload = payload;
      this._decryptedPayload = payload;
      if (payload.features) {
        this._options.features = payload.features;
      }
      if (payload.experiments) {
        this._options.experiments = payload.experiments;
        this._updateAllAutoExperiments();
      }
      this.ready = true;
      startStreaming(this, options);
      return this;
    }
    async init(options) {
      this._initialized = true;
      options = options || {};
      if (options.cacheSettings) {
        configureCache(options.cacheSettings);
      }
      if (options.payload) {
        await this.setPayload(options.payload);
        startStreaming(this, options);
        return {
          success: true,
          source: "init"
        };
      } else {
        const {
          data,
          ...res
        } = await this._refresh({
          ...options,
          allowStale: true
        });
        startStreaming(this, options);
        await this.setPayload(data || {});
        return res;
      }
    }

    /** @deprecated Use {@link init} */
    async loadFeatures(options) {
      options = options || {};
      await this.init({
        skipCache: options.skipCache,
        timeout: options.timeout,
        streaming: (this._options.backgroundSync ?? true) && (options.autoRefresh || this._options.subscribeToChanges)
      });
    }
    async refreshFeatures(options) {
      const res = await this._refresh({
        ...(options || {}),
        allowStale: false
      });
      if (res.data) {
        await this.setPayload(res.data);
      }
    }
    getApiInfo() {
      return [this.getApiHosts().apiHost, this.getClientKey()];
    }
    getApiHosts() {
      return getApiHosts(this._options);
    }
    getClientKey() {
      return this._options.clientKey || "";
    }
    getPayload() {
      return this._payload || {
        features: this.getFeatures(),
        experiments: this.getExperiments()
      };
    }
    getDecryptedPayload() {
      return this._decryptedPayload || this.getPayload();
    }
    isRemoteEval() {
      return this._options.remoteEval || false;
    }
    getCacheKeyAttributes() {
      return this._options.cacheKeyAttributes;
    }
    async _refresh({
      timeout,
      skipCache,
      allowStale,
      streaming
    }) {
      if (!this._options.clientKey) {
        throw new Error("Missing clientKey");
      }
      // Trigger refresh in feature repository
      return refreshFeatures({
        instance: this,
        timeout,
        skipCache: skipCache || this._options.disableCache,
        allowStale,
        backgroundSync: streaming ?? this._options.backgroundSync ?? true
      });
    }
    _render() {
      if (this._renderer) {
        try {
          this._renderer();
        } catch (e) {
          console.error("Failed to render", e);
        }
      }
    }

    /** @deprecated Use {@link setPayload} */
    setFeatures(features) {
      this._options.features = features;
      this.ready = true;
      this._render();
    }

    /** @deprecated Use {@link setPayload} */
    async setEncryptedFeatures(encryptedString, decryptionKey, subtle) {
      const featuresJSON = await decrypt(encryptedString, decryptionKey || this._options.decryptionKey, subtle);
      this.setFeatures(JSON.parse(featuresJSON));
    }

    /** @deprecated Use {@link setPayload} */
    setExperiments(experiments) {
      this._options.experiments = experiments;
      this.ready = true;
      this._updateAllAutoExperiments();
    }

    /** @deprecated Use {@link setPayload} */
    async setEncryptedExperiments(encryptedString, decryptionKey, subtle) {
      const experimentsJSON = await decrypt(encryptedString, decryptionKey || this._options.decryptionKey, subtle);
      this.setExperiments(JSON.parse(experimentsJSON));
    }
    async setAttributes(attributes) {
      this._options.attributes = attributes;
      if (this._options.stickyBucketService) {
        await this.refreshStickyBuckets();
      }
      if (this._options.remoteEval) {
        await this._refreshForRemoteEval();
        return;
      }
      this._render();
      this._updateAllAutoExperiments();
    }
    async updateAttributes(attributes) {
      return this.setAttributes({
        ...this._options.attributes,
        ...attributes
      });
    }
    async setAttributeOverrides(overrides) {
      this._options.attributeOverrides = overrides;
      if (this._options.stickyBucketService) {
        await this.refreshStickyBuckets();
      }
      if (this._options.remoteEval) {
        await this._refreshForRemoteEval();
        return;
      }
      this._render();
      this._updateAllAutoExperiments();
    }
    async setForcedVariations(vars) {
      this._options.forcedVariations = vars || {};
      if (this._options.remoteEval) {
        await this._refreshForRemoteEval();
        return;
      }
      this._render();
      this._updateAllAutoExperiments();
    }

    // eslint-disable-next-line
    setForcedFeatures(map) {
      this._options.forcedFeatureValues = map;
      this._render();
    }
    async setURL(url) {
      if (url === this._options.url) return;
      this._options.url = url;
      this._redirectedUrl = "";
      if (this._options.remoteEval) {
        await this._refreshForRemoteEval();
        this._updateAllAutoExperiments(true);
        return;
      }
      this._updateAllAutoExperiments(true);
    }
    getAttributes() {
      return {
        ...this._options.attributes,
        ...this._options.attributeOverrides
      };
    }
    getForcedVariations() {
      return this._options.forcedVariations || {};
    }
    getForcedFeatures() {
      // eslint-disable-next-line
      return this._options.forcedFeatureValues || new Map();
    }
    getStickyBucketAssignmentDocs() {
      return this._options.stickyBucketAssignmentDocs || {};
    }
    getUrl() {
      return this._options.url || "";
    }
    getFeatures() {
      return this._options.features || {};
    }
    getExperiments() {
      return this._options.experiments || [];
    }
    getCompletedChangeIds() {
      return Array.from(this._completedChangeIds);
    }
    subscribe(cb) {
      this._subscriptions.add(cb);
      return () => {
        this._subscriptions.delete(cb);
      };
    }
    async _refreshForRemoteEval() {
      if (!this._options.remoteEval) return;
      if (!this._initialized) return;
      const res = await this._refresh({
        allowStale: false
      });
      if (res.data) {
        await this.setPayload(res.data);
      }
    }
    getAllResults() {
      return new Map(this._assigned);
    }
    onDestroy(cb) {
      this._destroyCallbacks.push(cb);
    }
    isDestroyed() {
      return !!this._destroyed;
    }
    destroy(options) {
      options = options || {};
      this._destroyed = true;

      // Custom callbacks
      // Do this first in case it needs access to the below data that is cleared
      this._destroyCallbacks.forEach(cb => {
        try {
          cb();
        } catch (e) {
          console.error(e);
        }
      });

      // Release references to save memory
      this._subscriptions.clear();
      this._assigned.clear();
      this._trackedExperiments.clear();
      this._completedChangeIds.clear();
      this._deferredTrackingCalls.clear();
      this._trackedFeatures = {};
      this._destroyCallbacks = [];
      this._payload = undefined;
      this._saveStickyBucketAssignmentDoc = undefined;
      unsubscribe(this);
      if (options.destroyAllStreams) {
        clearAutoRefresh();
      }
      this.logs = [];
      if (isBrowser && window._growthbook === this) {
        delete window._growthbook;
      }

      // Undo any active auto experiments
      this._activeAutoExperiments.forEach(exp => {
        exp.undo();
      });
      this._activeAutoExperiments.clear();
      this._triggeredExpKeys.clear();
    }
    setRenderer(renderer) {
      this._renderer = renderer;
    }
    forceVariation(key, variation) {
      this._options.forcedVariations = this._options.forcedVariations || {};
      this._options.forcedVariations[key] = variation;
      if (this._options.remoteEval) {
        this._refreshForRemoteEval();
        return;
      }
      this._updateAllAutoExperiments();
      this._render();
    }
    run(experiment) {
      const {
        result
      } = runExperiment(experiment, null, this._getEvalContext());
      this._onExperimentEval(experiment, result);
      return result;
    }
    triggerExperiment(key) {
      this._triggeredExpKeys.add(key);
      if (!this._options.experiments) return null;
      const experiments = this._options.experiments.filter(exp => exp.key === key);
      return experiments.map(exp => {
        return this._runAutoExperiment(exp);
      }).filter(res => res !== null);
    }
    triggerAutoExperiments() {
      this._autoExperimentsAllowed = true;
      this._updateAllAutoExperiments(true);
    }
    _getEvalContext() {
      return {
        user: this._getUserContext(),
        global: this._getGlobalContext(),
        stack: {
          evaluatedFeatures: new Set()
        }
      };
    }
    _getUserContext() {
      return {
        attributes: this._options.user ? {
          ...this._options.user,
          ...this._options.attributes
        } : this._options.attributes,
        enableDevMode: this._options.enableDevMode,
        blockedChangeIds: this._options.blockedChangeIds,
        stickyBucketAssignmentDocs: this._options.stickyBucketAssignmentDocs,
        url: this._getContextUrl(),
        forcedVariations: this._options.forcedVariations,
        forcedFeatureValues: this._options.forcedFeatureValues,
        attributeOverrides: this._options.attributeOverrides,
        saveStickyBucketAssignmentDoc: this._saveStickyBucketAssignmentDoc,
        trackingCallback: this._options.trackingCallback,
        onFeatureUsage: this._options.onFeatureUsage,
        devLogs: this.logs,
        trackedExperiments: this._trackedExperiments,
        trackedFeatureUsage: this._trackedFeatures
      };
    }
    _getGlobalContext() {
      return {
        features: this._options.features,
        experiments: this._options.experiments,
        log: this.log,
        enabled: this._options.enabled,
        qaMode: this._options.qaMode,
        savedGroups: this._options.savedGroups,
        groups: this._options.groups,
        overrides: this._options.overrides,
        onExperimentEval: this._onExperimentEval,
        recordChangeId: this._recordChangedId,
        saveDeferredTrack: this._saveDeferredTrack,
        eventLogger: this._options.eventLogger
      };
    }
    _runAutoExperiment(experiment, forceRerun) {
      const existing = this._activeAutoExperiments.get(experiment);

      // If this is a manual experiment and it's not already running, skip
      if (experiment.manual && !this._triggeredExpKeys.has(experiment.key) && !existing) return null;

      // Check if this particular experiment is blocked by options settings
      // For example, if all visualEditor experiments are disabled
      const isBlocked = this._isAutoExperimentBlockedByContext(experiment);
      let result;
      let trackingCall;
      // Run the experiment (if blocked exclude)
      if (isBlocked) {
        result = getExperimentResult(this._getEvalContext(), experiment, -1, false, "");
      } else {
        ({
          result,
          trackingCall
        } = runExperiment(experiment, null, this._getEvalContext()));
        this._onExperimentEval(experiment, result);
      }

      // A hash to quickly tell if the assigned value changed
      const valueHash = JSON.stringify(result.value);

      // If the changes are already active, no need to re-apply them
      if (!forceRerun && result.inExperiment && existing && existing.valueHash === valueHash) {
        return result;
      }

      // Undo any existing changes
      if (existing) this._undoActiveAutoExperiment(experiment);

      // Apply new changes
      if (result.inExperiment) {
        const changeType = getAutoExperimentChangeType(experiment);
        if (changeType === "redirect" && result.value.urlRedirect && experiment.urlPatterns) {
          const url = experiment.persistQueryString ? mergeQueryStrings(this._getContextUrl(), result.value.urlRedirect) : result.value.urlRedirect;
          if (isURLTargeted(url, experiment.urlPatterns)) {
            this.log("Skipping redirect because original URL matches redirect URL", {
              id: experiment.key
            });
            return result;
          }
          this._redirectedUrl = url;
          const {
            navigate,
            delay
          } = this._getNavigateFunction();
          if (navigate) {
            if (isBrowser) {
              // Wait for the possibly-async tracking callback, bound by min and max delays
              Promise.all([...(trackingCall ? [promiseTimeout(trackingCall, this._options.maxNavigateDelay ?? 1000)] : []), new Promise(resolve => window.setTimeout(resolve, this._options.navigateDelay ?? delay))]).then(() => {
                try {
                  navigate(url);
                } catch (e) {
                  console.error(e);
                }
              });
            } else {
              try {
                navigate(url);
              } catch (e) {
                console.error(e);
              }
            }
          }
        } else if (changeType === "visual") {
          const undo = this._options.applyDomChangesCallback ? this._options.applyDomChangesCallback(result.value) : this._applyDOMChanges(result.value);
          if (undo) {
            this._activeAutoExperiments.set(experiment, {
              undo,
              valueHash
            });
          }
        }
      }
      return result;
    }
    _undoActiveAutoExperiment(exp) {
      const data = this._activeAutoExperiments.get(exp);
      if (data) {
        data.undo();
        this._activeAutoExperiments.delete(exp);
      }
    }
    _updateAllAutoExperiments(forceRerun) {
      if (!this._autoExperimentsAllowed) return;
      const experiments = this._options.experiments || [];

      // Stop any experiments that are no longer defined
      const keys = new Set(experiments);
      this._activeAutoExperiments.forEach((v, k) => {
        if (!keys.has(k)) {
          v.undo();
          this._activeAutoExperiments.delete(k);
        }
      });

      // Re-run all new/updated experiments
      for (const exp of experiments) {
        const result = this._runAutoExperiment(exp, forceRerun);

        // Once you're in a redirect experiment, break out of the loop and don't run any further experiments
        if (result && result.inExperiment && getAutoExperimentChangeType(exp) === "redirect") {
          break;
        }
      }
    }
    _onExperimentEval(experiment, result) {
      const prev = this._assigned.get(experiment.key);
      this._assigned.set(experiment.key, {
        experiment,
        result
      });
      if (this._subscriptions.size > 0) {
        this._fireSubscriptions(experiment, result, prev);
      }
    }
    _fireSubscriptions(experiment, result,
    // eslint-disable-next-line
    prev) {
      // If assigned variation has changed, fire subscriptions
      // TODO: what if the experiment definition has changed?
      if (!prev || prev.result.inExperiment !== result.inExperiment || prev.result.variationId !== result.variationId) {
        this._subscriptions.forEach(cb => {
          try {
            cb(experiment, result);
          } catch (e) {
            console.error(e);
          }
        });
      }
    }
    _recordChangedId(id) {
      this._completedChangeIds.add(id);
    }
    isOn(key) {
      return this.evalFeature(key).on;
    }
    isOff(key) {
      return this.evalFeature(key).off;
    }
    getFeatureValue(key, defaultValue) {
      const value = this.evalFeature(key).value;
      return value === null ? defaultValue : value;
    }

    /**
     * @deprecated Use {@link evalFeature}
     * @param id
     */

    feature(id) {
      return this.evalFeature(id);
    }
    evalFeature(id) {
      return evalFeature(id, this._getEvalContext());
    }
    log(msg, ctx) {
      if (!this.debug) return;
      if (this._options.log) this._options.log(msg, ctx);else console.log(msg, ctx);
    }
    getDeferredTrackingCalls() {
      return Array.from(this._deferredTrackingCalls.values());
    }
    setDeferredTrackingCalls(calls) {
      this._deferredTrackingCalls = new Map(calls.filter(c => c && c.experiment && c.result).map(c => {
        return [getExperimentDedupeKey(c.experiment, c.result), c];
      }));
    }
    async fireDeferredTrackingCalls() {
      if (!this._options.trackingCallback) return;
      const promises = [];
      this._deferredTrackingCalls.forEach(call => {
        if (!call || !call.experiment || !call.result) {
          console.error("Invalid deferred tracking call", {
            call: call
          });
        } else {
          promises.push(this._options.trackingCallback(call.experiment, call.result));
        }
      });
      this._deferredTrackingCalls.clear();
      await Promise.all(promises);
    }
    setTrackingCallback(callback) {
      this._options.trackingCallback = callback;
      this.fireDeferredTrackingCalls();
    }
    setFeatureUsageCallback(callback) {
      this._options.onFeatureUsage = callback;
    }
    setEventLogger(logger) {
      this._options.eventLogger = logger;
    }
    async logEvent(eventName, properties) {
      if (this._destroyed) {
        console.error("Cannot log event to destroyed GrowthBook instance");
        return;
      }
      if (this._options.enableDevMode) {
        this.logs.push({
          eventName,
          properties,
          timestamp: Date.now().toString(),
          logType: "event"
        });
      }
      if (this._options.eventLogger) {
        try {
          await this._options.eventLogger(eventName, properties || {}, this._getUserContext());
        } catch (e) {
          console.error(e);
        }
      } else {
        console.error("No event logger configured");
      }
    }
    _saveDeferredTrack(data) {
      this._deferredTrackingCalls.set(getExperimentDedupeKey(data.experiment, data.result), data);
    }
    _getContextUrl() {
      return this._options.url || (isBrowser ? window.location.href : "");
    }
    _isAutoExperimentBlockedByContext(experiment) {
      const changeType = getAutoExperimentChangeType(experiment);
      if (changeType === "visual") {
        if (this._options.disableVisualExperiments) return true;
        if (this._options.disableJsInjection) {
          if (experiment.variations.some(v => v.js)) {
            return true;
          }
        }
      } else if (changeType === "redirect") {
        if (this._options.disableUrlRedirectExperiments) return true;

        // Validate URLs
        try {
          const current = new URL(this._getContextUrl());
          for (const v of experiment.variations) {
            if (!v || !v.urlRedirect) continue;
            const url = new URL(v.urlRedirect);

            // If we're blocking cross origin redirects, block if the protocol or host is different
            if (this._options.disableCrossOriginUrlRedirectExperiments) {
              if (url.protocol !== current.protocol) return true;
              if (url.host !== current.host) return true;
            }
          }
        } catch (e) {
          // Problem parsing one of the URLs
          this.log("Error parsing current or redirect URL", {
            id: experiment.key,
            error: e
          });
          return true;
        }
      } else {
        // Block any unknown changeTypes
        return true;
      }
      if (experiment.changeId && (this._options.blockedChangeIds || []).includes(experiment.changeId)) {
        return true;
      }
      return false;
    }
    getRedirectUrl() {
      return this._redirectedUrl;
    }
    _getNavigateFunction() {
      if (this._options.navigate) {
        return {
          navigate: this._options.navigate,
          delay: 0
        };
      } else if (isBrowser) {
        return {
          navigate: url => {
            window.location.replace(url);
          },
          delay: 100
        };
      }
      return {
        navigate: null,
        delay: 0
      };
    }
    _applyDOMChanges(changes) {
      if (!isBrowser) return;
      const undo = [];
      if (changes.css) {
        const s = document.createElement("style");
        s.innerHTML = changes.css;
        document.head.appendChild(s);
        undo.push(() => s.remove());
      }
      if (changes.js) {
        const script = document.createElement("script");
        script.innerHTML = changes.js;
        if (this._options.jsInjectionNonce) {
          script.nonce = this._options.jsInjectionNonce;
        }
        document.head.appendChild(script);
        undo.push(() => script.remove());
      }
      if (changes.domMutations) {
        changes.domMutations.forEach(mutation => {
          undo.push(index.declarative(mutation).revert);
        });
      }
      return () => {
        undo.forEach(fn => fn());
      };
    }
    async refreshStickyBuckets(data) {
      if (this._options.stickyBucketService) {
        const ctx = this._getEvalContext();
        const docs = await getAllStickyBucketAssignmentDocs(ctx, this._options.stickyBucketService, data);
        this._options.stickyBucketAssignmentDocs = docs;
      }
    }
    generateStickyBucketAssignmentDocsSync(stickyBucketService, payload) {
      if (!("getAllAssignmentsSync" in stickyBucketService)) {
        console.error("generating StickyBucketAssignmentDocs docs requires StickyBucketServiceSync");
        return;
      }
      const ctx = this._getEvalContext();
      const attributes = getStickyBucketAttributes(ctx, payload);
      return stickyBucketService.getAllAssignmentsSync(attributes);
    }
    inDevMode() {
      return !!this._options.enableDevMode;
    }
  }

  /**
   * Responsible for reading and writing documents which describe sticky bucket assignments.
   */
  class StickyBucketService {
    constructor(opts) {
      opts = opts || {};
      this.prefix = opts.prefix || "";
    }
    /**
     * The SDK calls getAllAssignments to populate sticky buckets. This in turn will
     * typically loop through individual getAssignments calls. However, some StickyBucketService
     * instances (i.e. Redis) will instead perform a multi-query inside getAllAssignments instead.
     */
    async getAllAssignments(attributes) {
      const docs = {};
      (await Promise.all(Object.entries(attributes).map(([attributeName, attributeValue]) => this.getAssignments(attributeName, attributeValue)))).forEach(doc => {
        if (doc) {
          const key = getStickyBucketAttributeKey(doc.attributeName, doc.attributeValue);
          docs[key] = doc;
        }
      });
      return docs;
    }
    getKey(attributeName, attributeValue) {
      return `${this.prefix}${attributeName}||${attributeValue}`;
    }
  }
  class StickyBucketServiceSync extends StickyBucketService {
    async getAssignments(attributeName, attributeValue) {
      return this.getAssignmentsSync(attributeName, attributeValue);
    }
    async saveAssignments(doc) {
      this.saveAssignmentsSync(doc);
    }
    getAllAssignmentsSync(attributes) {
      const docs = {};
      Object.entries(attributes).map(([attributeName, attributeValue]) => this.getAssignmentsSync(attributeName, attributeValue)).forEach(doc => {
        if (doc) {
          const key = getStickyBucketAttributeKey(doc.attributeName, doc.attributeValue);
          docs[key] = doc;
        }
      });
      return docs;
    }
  }
  class LocalStorageStickyBucketService extends StickyBucketService {
    constructor(opts) {
      opts = opts || {};
      super();
      this.prefix = opts.prefix || "gbStickyBuckets__";
      try {
        this.localStorage = opts.localStorage || globalThis.localStorage;
      } catch (e) {
        // Ignore localStorage errors
      }
    }
    async getAssignments(attributeName, attributeValue) {
      const key = this.getKey(attributeName, attributeValue);
      let doc = null;
      if (!this.localStorage) return doc;
      try {
        const raw = (await this.localStorage.getItem(key)) || "{}";
        const data = JSON.parse(raw);
        if (data.attributeName && data.attributeValue && data.assignments) {
          doc = data;
        }
      } catch (e) {
        // Ignore localStorage errors
      }
      return doc;
    }
    async saveAssignments(doc) {
      const key = this.getKey(doc.attributeName, doc.attributeValue);
      if (!this.localStorage) return;
      try {
        await this.localStorage.setItem(key, JSON.stringify(doc));
      } catch (e) {
        // Ignore localStorage errors
      }
    }
  }
  class BrowserCookieStickyBucketService extends StickyBucketServiceSync {
    /**
     * Intended to be used with npm: 'js-cookie'.
     * Assumes:
     *  - reading a cookie is automatically decoded via decodeURIComponent() or similar
     *  - writing a cookie name & value is automatically encoded via encodeURIComponent() or similar
     *  - all cookie bodies are JSON encoded strings and are manually encoded/decoded
     */

    constructor({
      prefix = "gbStickyBuckets__",
      jsCookie,
      cookieAttributes = {
        expires: 180
      } // 180 days
    }) {
      super();
      this.prefix = prefix;
      this.jsCookie = jsCookie;
      this.cookieAttributes = cookieAttributes;
    }
    getAssignmentsSync(attributeName, attributeValue) {
      const key = this.getKey(attributeName, attributeValue);
      let doc = null;
      if (!this.jsCookie) return doc;
      try {
        const raw = this.jsCookie.get(key);
        const data = JSON.parse(raw || "{}");
        if (data.attributeName && data.attributeValue && data.assignments) {
          doc = data;
        }
      } catch (e) {
        // Ignore cookie errors
      }
      return doc;
    }
    async saveAssignmentsSync(doc) {
      const key = this.getKey(doc.attributeName, doc.attributeValue);
      if (!this.jsCookie) return;
      const str = JSON.stringify(doc);
      this.jsCookie.set(key, str, this.cookieAttributes);
    }
  }

  function getBrowserDevice(ua) {
    const browser = ua.match(/Edg/) ? "edge" : ua.match(/Chrome/) ? "chrome" : ua.match(/Firefox/) ? "firefox" : ua.match(/Safari/) ? "safari" : "unknown";
    const deviceType = ua.match(/Mobi/) ? "mobile" : "desktop";
    return {
      browser,
      deviceType
    };
  }
  function getURLAttributes(url) {
    if (!url) return {};
    return {
      url: url.href,
      path: url.pathname,
      host: url.host,
      query: url.search
    };
  }
  function autoAttributesPlugin(settings = {}) {
    // Browser only
    if (typeof window === "undefined") {
      throw new Error("autoAttributesPlugin only works in the browser");
    }
    const COOKIE_NAME = settings.uuidCookieName || "gbuuid";
    const uuidKey = settings.uuidKey || "id";
    let uuid = settings.uuid || "";
    function persistUUID() {
      setCookie(COOKIE_NAME, uuid);
    }
    function getUUID() {
      // Already stored in memory, return
      if (uuid) return uuid;

      // If cookie is already set, return
      uuid = getCookie(COOKIE_NAME);
      if (uuid) return uuid;

      // Generate a new UUID
      uuid = genUUID(window.crypto);
      return uuid;
    }

    // Listen for a custom event to persist the UUID cookie
    document.addEventListener("growthbookpersist", () => {
      persistUUID();
    });
    function getAutoAttributes(settings) {
      const ua = navigator.userAgent;
      const _uuid = getUUID();

      // If a uuid is provided, default persist to false, otherwise default to true
      if (settings.uuidAutoPersist ?? !settings.uuid) {
        persistUUID();
      }
      const url = location;
      return {
        ...getDataLayerVariables(),
        [uuidKey]: _uuid,
        ...getURLAttributes(url),
        pageTitle: document.title,
        ...getBrowserDevice(ua),
        ...getUtmAttributes(url)
      };
    }
    return gb => {
      // Only works for instances with user attributes
      if ("createScopedInstance" in gb) {
        return;
      }

      // Set initial attributes
      const attributes = getAutoAttributes(settings);
      attributes.url && gb.setURL(attributes.url);
      gb.updateAttributes(attributes);

      // Poll for URL changes and update GrowthBook
      let currentUrl = attributes.url;
      const intervalTimer = setInterval(() => {
        if (location.href !== currentUrl) {
          currentUrl = location.href;
          gb.setURL(currentUrl);
          gb.updateAttributes(getAutoAttributes(settings));
        }
      }, 500);

      // Listen for a custom event to update URL and attributes
      const refreshListener = () => {
        if (location.href !== currentUrl) {
          currentUrl = location.href;
          gb.setURL(currentUrl);
        }
        gb.updateAttributes(getAutoAttributes(settings));
      };
      document.addEventListener("growthbookrefresh", refreshListener);
      if ("onDestroy" in gb) {
        gb.onDestroy(() => {
          clearInterval(intervalTimer);
          document.removeEventListener("growthbookrefresh", refreshListener);
        });
      }
    };
  }
  function setCookie(name, value) {
    const d = new Date();
    const COOKIE_DAYS = 400; // 400 days is the max cookie duration for chrome
    d.setTime(d.getTime() + 24 * 60 * 60 * 1000 * COOKIE_DAYS);
    document.cookie = name + "=" + value + ";path=/;expires=" + d.toUTCString();
  }
  function getCookie(name) {
    const value = "; " + document.cookie;
    const parts = value.split(`; ${name}=`);
    return parts.length === 2 ? parts[1].split(";")[0] : "";
  }

  // Use the browsers crypto.randomUUID if set to generate a UUID
  function genUUID(crypto) {
    if (crypto && crypto.randomUUID) return crypto.randomUUID();
    return ("" + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => {
      const n = crypto && crypto.getRandomValues ? crypto.getRandomValues(new Uint8Array(1))[0] : Math.floor(Math.random() * 256);
      return (c ^ n & 15 >> c / 4).toString(16);
    });
  }
  function getUtmAttributes(url) {
    // Store utm- params in sessionStorage for future page loads
    let utms = {};
    try {
      const existing = sessionStorage.getItem("utm_params");
      if (existing) {
        utms = JSON.parse(existing);
      }
    } catch (e) {
      // Do nothing if sessionStorage is disabled (e.g. incognito window)
    }

    // Add utm params from querystring
    if (url && url.search) {
      const params = new URLSearchParams(url.search);
      let hasChanges = false;
      ["source", "medium", "campaign", "term", "content"].forEach(k => {
        // Querystring is in snake_case
        const param = `utm_${k}`;
        // Attribute keys are camelCase
        const attr = `utm` + k[0].toUpperCase() + k.slice(1);
        if (params.has(param)) {
          utms[attr] = params.get(param) || "";
          hasChanges = true;
        }
      });

      // Write back to sessionStorage
      if (hasChanges) {
        try {
          sessionStorage.setItem("utm_params", JSON.stringify(utms));
        } catch (e) {
          // Do nothing if sessionStorage is disabled (e.g. incognito window)
        }
      }
    }
    return utms;
  }
  function getDataLayerVariables() {
    if (typeof window === "undefined" || !window.dataLayer || !window.dataLayer.forEach) {
      return {};
    }
    const obj = {};
    window.dataLayer.forEach(item => {
      // Skip empty and non-object entries
      if (!item || typeof item !== "object" || "length" in item) return;

      // Skip events
      if ("event" in item) return;
      Object.keys(item).forEach(k => {
        // Filter out known properties that aren't useful
        if (typeof k !== "string" || k.match(/^(gtm)/)) return;
        const val = item[k];

        // Only add primitive variable values
        const valueType = typeof val;
        if (["string", "number", "boolean"].includes(valueType)) {
          obj[k] = val;
        }
      });
    });
    return obj;
  }

  const SDK_VERSION = loadSDKVersion();
  function parseString(value) {
    return typeof value === "string" ? value : null;
  }
  function parseAttributes(attributes) {
    const {
      user_id,
      device_id,
      anonymous_id,
      id,
      page_id,
      session_id,
      utmCampaign,
      utmContent,
      utmMedium,
      utmSource,
      utmTerm,
      pageTitle,
      ...nested
    } = attributes;
    return {
      nested,
      topLevel: {
        user_id: parseString(user_id),
        device_id: parseString(device_id || anonymous_id || id),
        page_id: parseString(page_id),
        session_id: parseString(session_id),
        utm_campaign: parseString(utmCampaign) || undefined,
        utm_content: parseString(utmContent) || undefined,
        utm_medium: parseString(utmMedium) || undefined,
        utm_source: parseString(utmSource) || undefined,
        utm_term: parseString(utmTerm) || undefined,
        page_title: parseString(pageTitle) || undefined
      }
    };
  }
  function getEventPayload({
    eventName,
    properties,
    attributes,
    url
  }) {
    const {
      nested,
      topLevel
    } = parseAttributes(attributes || {});
    return {
      event_name: eventName,
      properties_json: properties || {},
      ...topLevel,
      sdk_language: "js",
      sdk_version: SDK_VERSION,
      url: url,
      context_json: nested
    };
  }
  async function track({
    clientKey,
    ingestorHost,
    events
  }) {
    if (!events.length) return;
    const endpoint = `${ingestorHost || "https://us1.gb-ingest.com"}/track?client_key=${clientKey}`;
    const body = JSON.stringify(events);
    try {
      await fetch(endpoint, {
        method: "POST",
        body,
        headers: {
          Accept: "application/json",
          "Content-Type": "text/plain"
        },
        credentials: "omit"
      });
    } catch (e) {
      console.error("Failed to track event", e);
    }
  }
  function growthbookTrackingPlugin({
    queueFlushInterval = 100,
    ingestorHost,
    enable = true,
    debug,
    dedupeCacheSize = 1000,
    dedupeKeyAttributes = [],
    eventFilter
  } = {}) {
    return gb => {
      const clientKey = gb.getClientKey();
      if (!clientKey) {
        throw new Error("clientKey must be specified to use event logging");
      }

      // LRU cache for events to avoid duplicates
      const eventCache = new Set();
      if ("setEventLogger" in gb) {
        let _q = [];
        let timer = null;
        const flush = async () => {
          const events = _q;
          _q = [];
          timer && clearTimeout(timer);
          timer = null;
          events.length && (await track({
            clientKey,
            events,
            ingestorHost
          }));
        };
        let promise = null;
        gb.setEventLogger(async (eventName, properties, userContext) => {
          const data = {
            eventName,
            properties,
            attributes: userContext.attributes || {},
            url: userContext.url || ""
          };

          // Skip logging if the event is being filtered
          if (eventFilter && !eventFilter(data)) {
            return;
          }

          // De-dupe Feature Evaluated and Experiment Viewed events
          if (eventName === EVENT_FEATURE_EVALUATED || eventName === EVENT_EXPERIMENT_VIEWED) {
            // Build the key for de-duping
            const dedupeKeyData = {
              eventName,
              properties
            };
            for (const key of dedupeKeyAttributes) {
              dedupeKeyData["attr:" + key] = data.attributes[key];
            }
            const k = JSON.stringify(dedupeKeyData);
            // Duplicate event fired recently, move to end of LRU cache and skip
            if (eventCache.has(k)) {
              eventCache.delete(k);
              eventCache.add(k);
              return;
            }
            eventCache.add(k);

            // If the cache is too big, remove the oldest item
            if (eventCache.size > dedupeCacheSize) {
              const oldest = eventCache.values().next().value;
              oldest && eventCache.delete(oldest);
            }
          }
          const payload = getEventPayload(data);
          debug && console.log("Logging event to GrowthBook", JSON.parse(JSON.stringify(payload)));
          if (!enable) return;
          _q.push(payload);

          // Only one in-progress promise at a time
          if (!promise) {
            promise = new Promise((resolve, reject) => {
              // Flush the queue after a delay
              timer = setTimeout(() => {
                flush().then(resolve).catch(reject);
                promise = null;
              }, queueFlushInterval);
            });
          }
          await promise;
        });

        // Flush the queue on page unload
        if (typeof document !== "undefined" && document.visibilityState) {
          document.addEventListener("visibilitychange", () => {
            if (document.visibilityState === "hidden") {
              flush().catch(console.error);
            }
          });
        }

        // Flush the queue when the growthbook instance is destroyed
        "onDestroy" in gb && gb.onDestroy(() => {
          flush().catch(console.error);
        });
      }

      // Listen on window.gbEvents.push if in a browser
      // This makes it easier to integrate with Segment, GTM, etc.
      if (typeof window !== "undefined" && !("createScopedInstance" in gb)) {
        const prevEvents = Array.isArray(window.gbEvents) ? window.gbEvents : [];
        window.gbEvents = {
          push: event => {
            if ("isDestroyed" in gb && gb.isDestroyed()) {
              // If trying to log and the instance has been destroyed, switch back to just an array
              // This will let the next GrowthBook instance pick it up
              window.gbEvents = [event];
              return;
            }
            if (typeof event === "string") {
              gb.logEvent(event);
            } else if (event) {
              gb.logEvent(event.eventName, event.properties);
            }
          }
        };
        for (const event of prevEvents) {
          window.gbEvents.push(event);
        }
      }
    };
  }

  function thirdPartyTrackingPlugin({
    additionalCallback,
    trackers = ["gtag", "gtm", "segment"]
  } = {}) {
    // Browser only
    if (typeof window === "undefined") {
      throw new Error("thirdPartyTrackingPlugin only works in the browser");
    }
    return gb => {
      gb.setTrackingCallback(async (e, r) => {
        const promises = [];
        const eventParams = {
          experiment_id: e.key,
          variation_id: r.key
        };
        if (additionalCallback) {
          promises.push(Promise.resolve(additionalCallback(e, r)));
        }

        // GA4 - gtag
        if (trackers.includes("gtag") && window.gtag) {
          let gtagResolve;
          const gtagPromise = new Promise(resolve => {
            gtagResolve = resolve;
          });
          promises.push(gtagPromise);
          window.gtag("event", "experiment_viewed", {
            ...eventParams,
            event_callback: gtagResolve
          });
        }

        // GTM - dataLayer
        if (trackers.includes("gtm") && window.dataLayer) {
          let datalayerResolve;
          const datalayerPromise = new Promise(resolve => {
            datalayerResolve = resolve;
          });
          promises.push(datalayerPromise);
          window.dataLayer.push({
            event: "experiment_viewed",
            ...eventParams,
            eventCallback: datalayerResolve
          });
        }

        // Segment - analytics.js
        if (trackers.includes("segment") && window.analytics && window.analytics.track) {
          window.analytics.track("Experiment Viewed", eventParams);
          const segmentPromise = new Promise(resolve => window.setTimeout(resolve, 300));
          promises.push(segmentPromise);
        }
        await Promise.all(promises);
      });
    };
  }

  // Ensure dataLayer exists
  window.dataLayer = window.dataLayer || [];
  const currentScript = document.currentScript;
  const dataContext = currentScript ? currentScript.dataset : {};
  const windowContext = window.growthbook_config || {};
  let antiFlickerTimeout;
  function setAntiFlicker() {
    window.clearTimeout(antiFlickerTimeout);
    let timeoutMs = windowContext.antiFlickerTimeout ?? (dataContext.antiFlickerTimeout ? parseInt(dataContext.antiFlickerTimeout) : null) ?? 3500;
    if (!isFinite(timeoutMs)) {
      timeoutMs = 3500;
    }
    try {
      if (!document.getElementById("gb-anti-flicker-style")) {
        const styleTag = document.createElement("style");
        styleTag.setAttribute("id", "gb-anti-flicker-style");
        styleTag.innerHTML = ".gb-anti-flicker { opacity: 0 !important; pointer-events: none; }";
        document.head.appendChild(styleTag);
      }
      document.documentElement.classList.add("gb-anti-flicker");

      // Fallback if GrowthBook fails to load in specified time or 3.5 seconds.
      antiFlickerTimeout = window.setTimeout(unsetAntiFlicker, timeoutMs);
    } catch (e) {
      console.error(e);
    }
  }
  function unsetAntiFlicker() {
    window.clearTimeout(antiFlickerTimeout);
    try {
      document.documentElement.classList.remove("gb-anti-flicker");
    } catch (e) {
      console.error(e);
    }
  }
  if (windowContext.antiFlicker || dataContext.antiFlicker) {
    setAntiFlicker();
  }

  // Create sticky bucket service
  let stickyBucketService = undefined;
  if (windowContext.useStickyBucketService === "cookie" || dataContext.useStickyBucketService === "cookie") {
    stickyBucketService = new BrowserCookieStickyBucketService({
      prefix: windowContext.stickyBucketPrefix || dataContext.stickyBucketPrefix || undefined,
      jsCookie: api
    });
  } else if (windowContext.useStickyBucketService === "localStorage" || dataContext.useStickyBucketService === "localStorage") {
    stickyBucketService = new LocalStorageStickyBucketService({
      prefix: windowContext.stickyBucketPrefix || dataContext.stickyBucketPrefix || undefined
    });
  }
  const uuid = dataContext.uuid || windowContext.uuid;
  const plugins = [autoAttributesPlugin({
    uuid,
    uuidCookieName: windowContext.uuidCookieName || dataContext.uuidCookieName,
    uuidKey: windowContext.uuidKey || dataContext.uuidKey,
    uuidAutoPersist: !uuid && dataContext.noAutoCookies == null
  })];
  const tracking = dataContext.tracking || "gtag,gtm,segment";
  if (tracking !== "none") {
    const trackers = tracking.toLowerCase().split(",").map(t => t.trim());
    if (trackers.includes("growthbook")) {
      plugins.push(growthbookTrackingPlugin({
        ingestorHost: dataContext.eventIngestorHost
      }));
    }
    if (!windowContext.trackingCallback) {
      plugins.push(thirdPartyTrackingPlugin({
        additionalCallback: windowContext.additionalTrackingCallback,
        trackers: trackers
      }));
    }
  }

  // Create GrowthBook instance
  const gb = new GrowthBook({
    enableDevMode: true,
    ...dataContext,
    remoteEval: !!dataContext.remoteEval,
    ...windowContext,
    plugins,
    stickyBucketService
  });

  // Set the renderer to fire a custom DOM event
  // This will let us attach multiple listeners
  gb.setRenderer(() => {
    document.dispatchEvent(new CustomEvent("growthbookdata"));
  });
  gb.init({
    payload: windowContext.payload,
    streaming: !(windowContext.noStreaming || dataContext.noStreaming || windowContext.backgroundSync === false),
    cacheSettings: windowContext.cacheSettings
  }).then(() => {
    if (!(windowContext.antiFlicker || dataContext.antiFlicker)) return;
    if (gb.getRedirectUrl()) {
      setAntiFlicker();
    } else {
      unsetAntiFlicker();
    }
  });
  const fireCallback = cb => {
    try {
      cb && cb(gb);
    } catch (e) {
      console.error("Uncaught growthbook_queue error", e);
    }
  };

  // Process any queued callbacks
  if (window.growthbook_queue) {
    if (Array.isArray(window.growthbook_queue)) {
      window.growthbook_queue.forEach(cb => {
        fireCallback(cb);
      });
    }
  }
  // Replace the queue with a function that immediately calls the callback
  window.growthbook_queue = {
    push: cb => {
      fireCallback(cb);
    }
  };

  return gb;

})();
//# sourceMappingURL=auto.js.map
